diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml index c4ace5cad..50ddae4a5 100644 --- a/.github/workflows/check_artifacts.yml +++ b/.github/workflows/check_artifacts.yml @@ -35,12 +35,6 @@ jobs: restore-keys: | ${{ runner.os }}-cargo- - - name: Restore cached artifacts - uses: actions/cache@v4 - with: - path: artifacts - key: ${{ runner.os }}-artifacts-${{ hashFiles('**/Cargo.lock') }} - - name: Build Artifacts run: | docker run \ @@ -55,6 +49,17 @@ jobs: run: | $GITHUB_WORKSPACE/scripts/check_artifacts_size.sh + - name: Check Juno v1 artifact set + run: | + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_artifacts.py + + - name: Upload checked artifacts + uses: actions/upload-artifact@v4 + with: + name: cosmwasm-artifacts-${{ github.sha }} + path: artifacts + if-no-files-found: error + cosmwasm-check: @@ -72,12 +77,12 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - - name: Restore cached artifacts - uses: actions/cache/restore@v4 + + - name: Download checked artifacts + uses: actions/download-artifact@v4 with: + name: cosmwasm-artifacts-${{ github.sha }} path: artifacts - key: ${{ runner.os }}-artifacts-${{ hashFiles('**/Cargo.lock') }} - fail-on-cache-miss: true - name: Cosmwasm check run: | diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 000000000..2d03b96f0 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,54 @@ +name: Frontend CI + +on: + pull_request: + paths: + - "frontend/**" + - "services/indexer/**" + - ".github/workflows/frontend.yml" + push: + paths: + - "frontend/**" + - "services/indexer/**" + - ".github/workflows/frontend.yml" + +jobs: + validate: + name: Typecheck, lint, test, and build + runs-on: ubuntu-latest + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Playwright E2E + run: npm run test:e2e + + - name: Build + run: npm run build diff --git a/.github/workflows/frontend_deploy.yml b/.github/workflows/frontend_deploy.yml new file mode 100644 index 000000000..0b7c75be3 --- /dev/null +++ b/.github/workflows/frontend_deploy.yml @@ -0,0 +1,59 @@ +name: Frontend deploy + +on: + pull_request: + paths: + - "frontend/**" + - ".github/workflows/frontend_deploy.yml" + push: + branches: + - main + paths: + - "frontend/**" + - ".github/workflows/frontend_deploy.yml" + +permissions: + contents: read + deployments: write + pull-requests: read + +concurrency: + group: frontend-deploy-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + vercel: + name: Vercel preview/production + runs-on: ubuntu-latest + if: ${{ vars.VERCEL_ENABLED == 'true' }} + defaults: + run: + working-directory: frontend + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel environment + run: vercel pull --yes --environment=${{ github.event_name == 'push' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build Vercel output + run: vercel build ${{ github.event_name == 'push' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy Vercel output + run: vercel deploy --prebuilt ${{ github.event_name == 'push' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/indexer.yml b/.github/workflows/indexer.yml new file mode 100644 index 000000000..479b721d6 --- /dev/null +++ b/.github/workflows/indexer.yml @@ -0,0 +1,46 @@ +name: Indexer CI + +on: + pull_request: + paths: + - "services/indexer/**" + - ".github/workflows/indexer.yml" + push: + branches: + - main + paths: + - "services/indexer/**" + - ".github/workflows/indexer.yml" + +jobs: + validate: + name: Typecheck, test, build, and containerize + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/indexer + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: services/indexer/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Build Docker image + run: docker build -t astroport-juno-indexer:${{ github.sha }} . diff --git a/.github/workflows/release_artifacts.yml b/.github/workflows/release_artifacts.yml index 189b92635..cfb61e879 100644 --- a/.github/workflows/release_artifacts.yml +++ b/.github/workflows/release_artifacts.yml @@ -26,12 +26,6 @@ jobs: restore-keys: | ${{ runner.os }}-cargo- - - name: Restore cached artifacts - uses: actions/cache@v4 - with: - path: artifacts - key: ${{ runner.os }}-artifacts-${{ hashFiles('**/Cargo.lock') }} - - name: Build Artifacts run: | $GITHUB_WORKSPACE/scripts/build_release.sh diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index a7068f04d..8c4d3f971 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -22,6 +22,26 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 + + - name: Check Juno v1 launch guards + run: | + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_scope.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_schemas.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_deployment_template.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_tx_extractor.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_deployment_command.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_secret_scan.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_operator_checklist.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_dry_run_txs.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_deployment_gitignore.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_deployment_readme.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_frontend_config.py + python3 $GITHUB_WORKSPACE/scripts/generate_juno_v1_frontend_types.py --check + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_frontend_example.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_frontend_handoff_sync.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_frontend_release_checklist.py + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_ci_wiring.py + - uses: actions/cache@v4 if: always() with: @@ -57,5 +77,6 @@ jobs: - name: Generate and check schemas run: | $GITHUB_WORKSPACE/scripts/build_schemas.sh + python3 $GITHUB_WORKSPACE/scripts/check_juno_v1_schemas.py git add -A $GITHUB_WORKSPACE/schemas # consider new contract schemas git diff-index --cached HEAD --exit-code -- diff --git a/.gitignore b/.gitignore index 1ca0e3bf2..87fbe9a67 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Build results target/ +/services/indexer/dist/ # IDEs .vscode/ @@ -15,8 +16,15 @@ target/ # Auto-gen .cargo-ok /scripts/.env -*/node_modules/* +**/node_modules/ #/scripts/package-lock.json /artifacts/ /contracts/**/schema/ /e2e/config.json +/deployment/tx/ +/deployment/juno-v1-testnet.json +/deployment/juno-v1-mainnet.json + +# env +.env +.env.local diff --git a/ACCESSIBILITY_AUDIT.md b/ACCESSIBILITY_AUDIT.md new file mode 100644 index 000000000..2d9906ec9 --- /dev/null +++ b/ACCESSIBILITY_AUDIT.md @@ -0,0 +1,51 @@ +# JUNO DEX Accessibility Validation + +Date: 2026-07-13 +Target: WCAG 2.2 Level AA +Scope: swap, pools, pool details, portfolio, liquidity, pool creation, transaction review/lifecycle, token selection, settings, and responsive navigation. + +## Automated evidence + +- Serious and critical axe findings fail Playwright across `/swap`, `/pools`, `/portfolio`, `/create`, `/liquidity`, and `/stats`. +- Transaction review, wallet rejection, and confirmed transaction states receive the same axe gate. +- The token selector dialog receives a dedicated axe gate. +- A rendered-style audit fails if visible informative text on any core route is below 12 CSS pixels. +- The 320 CSS-pixel viewport has no horizontal overflow and primary navigation, wallet, transaction, and quick-navigation controls are at least 44 pixels high. +- Keyboard checks cover skip navigation, route focus, token selector focus entry/return, settings Escape/return, and modal focus containment. +- Charts expose one keyboard focus target and an accessible data summary rather than one tab stop per candle. +- Reduced-motion emulation verifies that confirmed-transaction feedback collapses to no meaningful animation. +- Sort state, live transaction status, quote refresh state, and page changes have programmatic semantics. + +Current automated result: all 16 Playwright checks pass together in one run, including the six-route axe sweep with its appropriate 60-second scope timeout. + +## Manual release checks still required + +These checks require human perception or assistive-technology behavior and must be recorded before claiming full WCAG 2.2 AA conformance: + +- Complete swap, add/remove liquidity, incentives, and pool creation with current NVDA + Firefox or Chrome. +- Repeat the primary swap and transaction-recovery journey with current VoiceOver + Safari on macOS and iOS. +- Confirm announcement order and usefulness for quote refresh, validation errors, wallet rejection, timeout, confirmation, and delayed indexing. +- Inspect focus visibility and reading order in Windows High Contrast/forced-colors mode. +- Inspect text and non-text contrast for hover, focus, disabled, stale, warning, danger, verified, and success states with a contrast analyzer. +- Validate browser text-only zoom to 200% and desktop page zoom to 400% without clipped controls or lost content. +- Validate target spacing and fixed navigation with touch exploration on a physical narrow-screen device. + +Record browser, OS, assistive-technology version, tester, date, outcome, and linked defects for each run. `GOAL.md` keeps full WCAG 2.2 AA unchecked until this manual evidence is complete. + +## Evidence record + +Use `Pass`, `Fail`, or `N/A` with an explanation. A tool name without tester, version, date, and observed result is not evidence. + +| Environment | Journey/state | Tester and date | Result | Defect or evidence link | +| --- | --- | --- | --- | --- | +| NVDA + Firefox (current) | Swap, review, rejection, timeout, confirmation | | | | +| NVDA + Chrome (current) | Add/remove liquidity, rewards, pool creation | | | | +| VoiceOver + macOS Safari (current) | Swap, review, recovery, activity | | | | +| VoiceOver + iOS Safari (current) | Swap, navigation, review, recovery | | | | +| Windows forced colors | All core routes and transaction states | | | | +| Contrast analyzer | Text/non-text interactive states | | | | +| Desktop browser at 200% text zoom | All core routes | | | | +| Desktop browser at 400% page zoom | All core routes | | | | +| Physical narrow-screen touch device | Navigation, dialogs, forms, activity | | | | + +For screen-reader journeys, record whether labels identify asset and action, changed content is announced once in a useful order, review bounds are understandable without visual context, focus never becomes lost or trapped, and the user can distinguish safe retry from an ambiguous submitted transaction. diff --git a/GOAL.md b/GOAL.md new file mode 100644 index 000000000..c6a9c436c --- /dev/null +++ b/GOAL.md @@ -0,0 +1,202 @@ +# Goal: A Safe, Clear, and Delightful JUNO DEX + +## Outcome + +Ship a public JUNO DEX frontend in which a first-time trader can confidently understand and complete a swap, a liquidity provider can safely manage a position, and every user can recover from wallet, network, quote, and transaction failures without guessing. + +The interface must be truthful before it is clever, protective before it is fast, and progressively disclose technical detail without forcing protocol jargon into the primary journey. + +The supporting findings and evidence are in [UX_AUDIT.md](./UX_AUDIT.md). + +## Product principles + +1. **Never overstate certainty.** Unknown verification, impact, fees, health, and price data must be labeled unknown—not inferred as safe or live. +2. **Show the commitment before the signature.** Users see human-readable inputs, bounds, fees, account, network, route, and risks before opening the wallet. +3. **Prevent costly mistakes.** Extreme impact, slippage, stale quotes, wrong networks, malicious assets, and unsupported token mechanics are gated or blocked. +4. **Keep the main task calm.** Optional analytics degrade quietly; trade-blocking problems are prominent and actionable. +5. **Make recovery obvious.** Every transaction has a durable status, explorer path, and safe next step. +6. **Design for touch, keyboard, and assistive technology together.** WCAG 2.2 AA is a release requirement, not a later polish pass. + +## Public-launch release gates + +All checkboxes in this section must be complete before a public mainnet launch. + +### Gate 1 — Truthful markets and assets + +- [x] Require explicit `verified: true` for verified pools and assets; default missing status to unverified. +- [x] Add pool lifecycle state (`experimental`, `active`, `deprecated`, `blocked`) and validate it in registry parsing. +- [x] Remove experimental/test pools from default selection, featured markets, and normal routing. +- [x] Keep selected-token verification and origin visible through review and signature. +- [x] Feed live reserves and registry risk metadata into route assessment. +- [x] Hard-block known malicious/blocked denoms; do not offer an acknowledgement override. +- [x] Add unit tests proving unknown/experimental/blocked assets cannot appear verified or route by default. + +**Acceptance:** no pool or asset receives a positive trust label solely because it came from the local registry; the initial swap market is an explicitly active, production-approved pair. + +- [ ] **Gate 1 acceptance evidence:** configure and approve at least one explicitly active production market; `npm run release:check` must pass on the release commit. + +Progress (2026-07-14): registry parsing requires explicit pool/asset verification and lifecycle metadata; factory-discovered pools default to `experimental` and unverified; route construction accepts only enabled `active` pools. Selected assets retain verification and native/IBC/CW20 origin in the swap and review. Route assessment consumes live reserves and registry metadata, and its badges remain visible at commitment time. Explicitly blocked assets are rejected by registry market selection, route construction, swap, liquidity, and pool creation with no acknowledgement override. The committed registry now contains five enabled, active, explicitly verified markets and passes `npm run release:check`. Operator/security provenance approval still needs to be recorded in `RELEASE_EVIDENCE.md` before Gate 1 acceptance is closed. + +### Gate 2 — Safe and truthful swap execution + +- [x] Replace the current `exact output` claim with truthful target-output language, or implement maximum-input-protected exact-output execution. +- [x] Always show the effective custom/persisted slippage value in the quote and review. +- [x] Reduce the custom-slippage ceiling and add escalating warnings/confirmation at product-defined thresholds. +- [x] Show price impact for every route where it can be computed. +- [x] Require explicit confirmation for high impact and hard-block pathological execution by default. +- [x] Display minimum received for exact-in and maximum sent for genuine exact-out. +- [x] Disable expired quotes and refresh/revalidate immediately before review. +- [x] Bind confirmation to the reviewed amount, route, slippage, and quote version. +- [x] Add tests for high/extreme impact, custom 50% legacy storage, stale quote, route change during review, and exact-output semantics. + +**Acceptance:** the transaction cannot execute under a materially different or less protective interpretation than the one displayed in the review. + +Progress (2026-07-13): reverse simulation is now presented as a target-output estimate and still executes with a visible minimum-received bound. Effective slippage is always visible in quote and review, capped at 5%, and requires acknowledgement above 1%; legacy 50% storage is clamped and tested. Direct-route impact is classified with acknowledgement from 5% and a hard block from 15%; unavailable multi-hop impact is labeled and acknowledged. Quotes expose expiry, expired quotes are blocked, review forces a fresh simulation, and wallet confirmation is bound to the reviewed amount, route, slippage, and quote timestamp. Focused Gate 2 coverage passes across swap, quote, slippage math, and persisted settings; the complete frontend suite passes 231 tests across 46 files. + +### Gate 3 — Complete pre-signature review + +- [x] Build a reusable review sheet for swap, add liquidity, remove liquidity, incentives, and create pool. +- [x] Show human-formatted send/receive amounts and execution bounds. +- [x] Show connected account, chain ID, token/pool status, route, and contract disclosure. +- [x] Show LP/protocol commission where applicable and an exact-message network-fee estimate in JUNO; add fiat only when reliable. +- [x] Explain whether values are fixed, estimated, minimum, or maximum. +- [x] Label unsupported/unavailable impact or fee data and add appropriate friction. +- [x] Make the final action read `Confirm in wallet` and refresh safety-critical data first. + +**Acceptance:** in usability testing, at least 4 of 5 first-time participants can correctly state what they send, the worst acceptable outcome, the fee categories, the account/network, and why any warning appears before signing. + +- [ ] **Gate 3 acceptance evidence:** record a passing five-participant moderated study using `RELEASE_EVIDENCE.md`. + +Progress (2026-07-13): a shared transaction-review surface now gates swap, add liquidity, remove liquidity, stake/unstake/claim, and pool creation. It consistently exposes the connected account, chain, fixed/estimated/enforced amounts, relevant pool/trading commissions, technical destinations, and the final `Confirm in wallet` action. Swap refreshes its quote; liquidity refreshes reserves; incentives refreshes contract state; pool creation refreshes factory configuration and duplicate detection. Every review simulates the exact execution message and displays the resulting JUNO fee estimate using the configured gas price and a 1.3 gas adjustment; unsupported wallet clients get an explicit unavailable state. Fiat is omitted because no reliable price feed is configured. The moderated 4-of-5 comprehension criterion remains open, so Gate 3 is not yet complete. + +### Gate 4 — Durable transaction lifecycle and recovery + +- [x] Use one shared state model: preparing, awaiting signature, submitted, confirmed, rejected, failed, and timed out. +- [x] Render one consistent lifecycle surface and one toast stream per transaction. +- [x] Link every transaction hash to the configured explorer. +- [x] Persist pending/recent transactions across route changes and modal closure. +- [x] Show human-formatted amounts in success copy. +- [x] On timeout, check hash/account sequence before allowing rebroadcast. +- [x] Provide `View in explorer`, `Refresh balances`, and safe retry actions where applicable. +- [x] Update balances, positions, and activity optimistically only when the state is unambiguous. +- [x] Repair mocked-wallet E2E coverage for swap, add/remove liquidity, incentives, portfolio, and pool creation. +- [x] Add browser coverage for rejection, timeout/delayed indexing, and duplicate prevention. + +**Acceptance:** users never need to resubmit merely because the app lost visible status, and a timeout path cannot blindly duplicate an irreversible action. + +Progress (2026-07-13): every exposed write flow now renders the shared lifecycle card and relies on the transaction runner's single toast stream. The lifecycle vocabulary covers preparation through confirmation/failure, hashes link to the configured explorer, and a global local-storage-backed transaction center survives route/modal unmounts. Confirmed transactions offer refresh actions and human amounts. Ambiguous timeouts explicitly require checking recent account activity and never expose blind retry. An in-flight promise guard deduplicates rapid confirmation, including while downstream data is reconciling. Confirmed activity is written to durable local history immediately. Cached balances reconcile only protocol-exact non-gas deltas: non-JUNO native/IBC/TokenFactory spends, LP burns, and fixed LP stake/unstake amounts. JUNO spends, CW20 balances, swap receipts, LP mints, and rewards wait for authoritative refresh because their final deltas are not known locally. The mocked-wallet browser suite exercises normal write journeys plus wallet rejection, pre-hash broadcast failure and safe retry at mobile width, ambiguous timeout/delayed-indexing copy, and double-confirm prevention. + +### Gate 5 — Supported asset mechanics only + +- [x] Audit native, IBC, TokenFactory, and CW20 execution paths for swap and liquidity actions. +- [x] Hide or disable asset/action combinations without a verified transaction path. +- [x] Implement atomic CW20 send hooks where supported. +- [x] If approval is required, request an exact amount and show spender, token, allowance, transaction count, and revoke guidance. +- [x] Never request unlimited approval by default. +- [x] Add integration tests for each exposed asset kind and route type. + +**Acceptance:** every asset/action combination presented by the UI has a tested, understandable, and safely bounded execution path. + +Progress (2026-07-13): native, IBC, and TokenFactory denoms use bounded native-fund messages. Direct and routed CW20 swaps use atomic CW20 `send` hooks to the pair/router instead of unfunded executes. CW20 add-liquidity is disabled in the UI and rejected again at message construction until an exact-allowance flow exists; no approval path requests any allowance, unlimited or otherwise. The 16-case exact-message integration matrix encodes native, IBC, TokenFactory, and CW20 direct and multi-hop swaps; supported provide/withdraw combinations; the rejected CW20-liquidity boundary; and pool creation identity for every asset kind. + +## Launch-quality work + +Complete after the loss-prevention gates, before calling the product delightful. + +### Core-flow usability + +- [x] Start swap amounts empty; do not imply transaction intent. +- [x] Make disconnected/wrong-network primary actions connect or switch, then preserve and revalidate intent. +- [x] Show both token balances and block execution while the relevant balance is unknown. +- [x] Add gas-aware MAX and 50% shortcuts to the send field. +- [x] Visibly distinguish `Sell exact` and `Target buy`/genuine `Buy exact` modes. +- [x] Keep the last valid quote visible but subdued while refreshing to avoid layout shifts. +- [x] Rename the pools-page `Provide` link so it matches its destination, or route it to real add liquidity. +- [x] Show liquidity empty/helper states only when the wallet actually has no positions. + +### Information architecture and content + +- [x] Rewrite user-facing operator jargon into plain outcomes and recovery actions. +- [x] Move raw contracts, denoms, RPC details, query status, and implementation caveats behind disclosures. +- [x] Reorganize pool details around performance, composition, risk, the user's position, and primary management actions. +- [x] Replace immediate wallet-chip disconnect with an account menu containing address, copy, explorer, switch, and disconnect. +- [x] Show actual wallet chain, RPC health, indexer freshness, fallback state, and last update separately. +- [x] Collapse unavailable optional analytics into quiet placeholders; use stale cached data with timestamps when safe. + +Progress (2026-07-13): pool details now lead with the user's position and management actions, followed by explicit pool risk, performance, reserve composition, price history, and recent activity. Contract addresses, asset identifiers, LP supply math, network endpoints, model parameters, and implementation caveats are consolidated in labeled disclosures. Missing chart data is a quiet, retryable placeholder rather than an alert, and cached price and performance data carries an updated or last-available timestamp. The network surface separately reports the wallet's actual and required chain, transaction readiness, live RPC/fallback health, and whether the indexer is healthy, unavailable, disabled, unconfigured, or serving preview data. Liquidity, pool creation, analytics, incentives, and portfolio states now describe user outcomes and recovery actions instead of factory/query/indexer wiring. + +### Mobile and accessibility + +- [ ] Meet WCAG 2.2 AA for all core routes and transaction states. +- [x] Raise informative text contrast to at least 4.5:1 and establish a readable minimum text size. +- [x] Use approximately 44 px touch targets for primary mobile controls. +- [x] Combine mobile brand/navigation/account chrome into one compact sticky bar; evaluate bottom navigation. +- [x] Add skip-to-content, document-title updates, route announcements, and focus management. +- [x] Give sort columns visible direction and programmatic `aria-sort`. +- [x] Replace per-candle tab stops with a single chart focus target and accessible summary table. +- [x] Give settings proper popover/modal focus, Escape, outside-click, and focus-return behavior. +- [x] Prevent toasts from obscuring content; auto-dismiss noncritical notices with pause on hover/focus. +- [x] Fail accessibility CI on serious and critical axe findings across core routes and the token selector. +- [x] Add dedicated WCAG 2.2 mobile, keyboard, zoom-equivalent reflow, and focus tests. + +Progress (2026-07-13): core routes, the token selector, transaction review, rejection, and confirmation states pass the serious/critical WCAG 2.2 axe gate. A rendered-style audit enforces at least 12 CSS pixels for visible informative text across every core route; axe enforces AA text contrast, including disabled transaction controls. Browser tests cover skip navigation, route focus, token/settings dialog focus return, 320 CSS-pixel reflow (the WCAG desktop zoom equivalent), 44 px mobile controls, and reduced motion. Sort direction is visible and programmatic, and the chart uses one focus target with a summary table instead of a tab stop per candle. Mobile brand, wallet, and navigation now share one compact sticky header, with a tested bottom quick bar for Swap, Pools, Portfolio, and durable transaction activity. Full AA remains open pending the assistive-technology and perceptual checks documented in `ACCESSIBILITY_AUDIT.md`. + +### Delight and perceived quality + +- [x] Use subtle quote-freshness feedback without creating urgency theater. +- [x] Add stable loading/success transitions that do not shift the form. +- [x] Use restrained semantic color differentiation for success, warning, danger, and verification. +- [x] Add brief successful-transaction feedback that respects reduced-motion preferences. +- [x] Keep recent activity, portfolio, and transaction status fast to reach on mobile. +- [x] Ensure secondary service failures never make a healthy trading flow look broken. + +Progress (2026-07-13): success, warning, danger, pending, and verification states use restrained semantic tokens plus text or icons rather than color alone. Confirmed transactions receive a brief checkmarked toast that auto-dismisses and collapses its animation under `prefers-reduced-motion`. The quote region reserves stable space from empty input through simulation, the browser suite caps normal quote-to-action movement at 8 pixels, and transaction lifecycle cards render after primary actions so status expansion does not move the commitment control. Optional market discovery, activity, price-history, portfolio enrichment, and ranking failures render as quiet retryable notices while healthy balances and trading actions remain visually primary. On narrow screens, persistent quick navigation keeps Portfolio and the durable transaction center one tap away without obscuring main content. + +## Test matrix + +Current traceability and missing evidence are recorded in [RELEASE_EVIDENCE.md](./RELEASE_EVIDENCE.md). This matrix is not complete merely because representative component and browser suites pass. + +Each write flow must be covered at desktop and mobile widths for: + +- disconnected, connected, wrong chain, and chain-switch rejection; +- empty, loading, stale, unavailable, and refreshed data; +- insufficient, exact, and ample balances; +- verified, unknown, experimental, and blocked assets/pools; +- normal, elevated, high, and pathological price impact; +- preset, custom, and legacy-persisted slippage; +- wallet rejection, broadcast failure, timeout, confirmation, and delayed indexing; +- keyboard-only, 200% zoom/reflow, reduced motion, and screen-reader status announcements. + +## Success measures + +Instrument without collecting wallet-identifying analytics beyond what is necessary and consented to. + +- **Safety:** zero known cases where displayed verification or execution semantics differ from the signed transaction; zero blind timeout rebroadcasts. +- **Comprehension:** >=80% of usability-test participants correctly identify execution bounds, fees, network, and token status before signing. +- **Completion:** >=90% of successful wallet-signature journeys retain visible status through chain confirmation. +- **Recovery:** >=80% of participants can recover from wrong-network, rejected-signature, stale-quote, and delayed-indexing scenarios without assistance. +- **Accessibility:** zero serious/critical axe violations on gated flows, full keyboard completion, and documented WCAG 2.2 AA manual checks. +- **Responsiveness:** no horizontal overflow at 320 px; primary controls meet target-size requirements; key swap actions stay reachable without obstructive overlays. +- **Perceived quality:** optional-data degradation does not reduce swap completion in tests or produce a trade-blocking visual treatment. + +## Delivery order + +1. Gates 1-2: truth and loss prevention. +2. Gates 3-4: review, status, and recovery. +3. Gate 5: supported token mechanics. +4. Core-flow and content simplification. +5. WCAG/mobile release pass. +6. Delight, performance, usability testing, and metric validation. + +## Definition of done + +This goal is complete when: + +- every public-launch gate and launch-quality checkbox is complete; +- the complete test matrix passes in CI where automatable and has documented manual evidence otherwise; +- a final heuristic review finds no critical/high issue; +- moderated tests with at least five representative users meet the comprehension and recovery targets; +- production configuration contains no experimental default market or falsely positive health/verification state; +- product, engineering, design, accessibility, and security owners explicitly sign off on launch readiness. + +The reproducible commands, test-matrix gaps, moderated-study protocol, production checklist, and sign-off record are maintained in [RELEASE_EVIDENCE.md](./RELEASE_EVIDENCE.md). diff --git a/README.md b/README.md index 6967c228b..9d2a38f3d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@ -# Astroport Core +# Juno Dex [![codecov](https://codecov.io/gh/astroport-fi/astroport-core/branch/main/graph/badge.svg?token=ROOLZTGZMM)](https://codecov.io/gh/astroport-fi/astroport-core) -Multi pool type automated market-maker (AMM) protocol powered by smart contracts on the Terra, Injective, Neutron, -and Osmosis -blockchains. - -## Contracts diagram - -![contract diagram](./assets/sc_diagram.png "Contracts Diagram") +Forked with love by agents from [astroport-core](https://github.com/astroport-fi/astroport-core). ## General Contracts diff --git a/RELEASE_EVIDENCE.md b/RELEASE_EVIDENCE.md new file mode 100644 index 000000000..b325008df --- /dev/null +++ b/RELEASE_EVIDENCE.md @@ -0,0 +1,80 @@ +# JUNO DEX Release Evidence + +Date opened: 2026-07-13 +Status: **not ready for public launch** + +This file is the evidence index for `GOAL.md`. A checkbox is complete only when the linked automated result or signed human record exists. Passing automated tests does not substitute for usability, assistive-technology, production-configuration, or owner approval evidence. + +## Automated evidence + +| Requirement | Evidence | Current result | +| --- | --- | --- | +| Source behavior and exact-message integration | `npm test -- --run` | Pass: 231 tests in 46 files | +| Type safety | `npm run typecheck` | Pass | +| Core mocked-wallet journeys and recovery | `npx playwright test e2e/dex-flows.spec.ts` | Pass | +| Serious/critical axe, keyboard, reflow, text size, focus, reduced motion | `npx playwright test e2e/a11y.spec.ts` | Pass | +| Combined browser regression | `npx playwright test` | Pass: 16 tests | +| Production registry readiness | `npm run release:check` | Pass: 5 explicitly active markets; provenance approval remains a human release check | + +## Test-matrix traceability + +The matrix in `GOAL.md` describes states that apply differently to each write flow. The current suite provides strong shared-component and representative browser coverage, but it does **not** exercise every listed state at both desktop and mobile for every write flow. Until the missing rows are automated or recorded manually, the complete test-matrix Definition of Done remains open. + +| Matrix area | Current evidence | Remaining evidence | +| --- | --- | --- | +| Wallet/network | App and wallet component tests cover disconnected, connected, wrong-chain, and switching behavior; browser tests cover connected flows and wallet rejection. | Browser completion for disconnected intent preservation and chain-switch rejection at desktop and mobile. | +| Data lifecycle | Query/data-access tests cover loading, stale, unavailable, refreshed, and fallback behavior. | Rendered desktop/mobile evidence for each write flow where the state changes the commitment action. | +| Balances | Swap/liquidity component tests cover unknown, insufficient, and available balances. | Exact-balance and mobile rendered evidence for each amount-bearing write flow. | +| Asset/pool trust | Registry, risk, route, swap, liquidity, and creation tests cover verified, unknown, experimental, and blocked data. | Browser evidence for warning comprehension; blocked assets must remain non-actionable. | +| Price impact | Swap tests cover normal, elevated/high acknowledgement, unavailable multi-hop impact, and pathological blocking. | Mobile rendered evidence for each applicable impact class. | +| Slippage | Slippage/settings/swap tests cover presets, custom bounds, warnings, and legacy 50% clamping. | Mobile keyboard/screen-reader review of custom-warning interaction. | +| Transaction outcomes | Runner/component/browser tests cover rejection, pre-hash broadcast failure, timeout, confirmation, delayed indexing, and duplicate prevention. | Mobile recovery evidence for each remaining materially distinct recovery action. | +| Accessibility | Axe, keyboard focus, 320 px reflow, target size, reduced motion, and status-state checks pass. | The manual checks listed in `ACCESSIBILITY_AUDIT.md`, including NVDA and VoiceOver completion. | + +## Moderated usability protocol + +Recruit at least five representative first-time or infrequent DeFi users. Do not coach terminology. Use a production-like build with non-spendable test assets and record participant consent without retaining wallet addresses. + +For each participant, run these tasks: + +1. Prepare a normal swap and pause at review. Ask what will be sent, the worst acceptable received amount, fee categories, account/network, asset status, and route. +2. Present an unverified/high-impact case. Ask why the warning appears and whether proceeding is appropriate. +3. Recover from wrong network, wallet rejection, an expired quote, and delayed indexing without assistance. +4. Add and remove liquidity, asking which amounts are fixed, estimated, or minimum-protected. +5. Locate confirmed activity and the explorer path after navigating away, including once at mobile width. + +Record one row per participant; never infer an answer from successful clicking. + +| Participant | Representative profile | Commitment comprehension (pass/fail) | Recovery (pass/fail) | Critical confusion or loss-risk behavior | Notes/evidence link | +| --- | --- | --- | --- | --- | --- | +| P1 | | | | | | +| P2 | | | | | | +| P3 | | | | | | +| P4 | | | | | | +| P5 | | | | | | + +Required result: at least 4/5 pass commitment comprehension and at least 4/5 recover without assistance, with no unresolved critical/high heuristic issue introduced by observed behavior. + +## Manual accessibility evidence + +Complete the browser/assistive-technology matrix in `ACCESSIBILITY_AUDIT.md`. Link defects and rerun evidence here. Do not check full WCAG 2.2 AA in `GOAL.md` until every applicable criterion has a recorded pass or justified not-applicable determination. + +## Production readiness record + +- [x] `npm run release:check` passes against the current candidate (rerun against the exact release commit). +- [ ] At least one enabled, active, explicitly verified market and both assets have operator/security provenance approval. +- [ ] RPC, REST, explorer, indexer, factory, router, incentives, and oracle values are validated against the intended JUNO deployment. +- [ ] A production-like smoke test confirms health labels never report healthy/verified from preview, mock, empty, or failed responses. +- [ ] Release artifact digest/commit: + +## Required sign-offs + +Sign only after reviewing the exact release commit and linked evidence. + +| Owner | Name | Decision | Date | Evidence/conditions | +| --- | --- | --- | --- | --- | +| Product | | | | | +| Engineering | | | | | +| Design | | | | | +| Accessibility | | | | | +| Security | | | | | diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..99776caf6 --- /dev/null +++ b/TODO.md @@ -0,0 +1,16 @@ +UI Cleanup: +- Clean up Swap form. No "Transaction status" "Awaiting wallet signature" "Swapping JUNO for JUNOAGENT-TEST on Juno…" "Retry transaction" "Request rejected" Tx hash, etc. Those don't belong in the swap form. +- Remove querying route from swap form, it shifts layout +- On pools detail page, clean up price chart component. No candles count or "Latest available". Review code and try to make the best UX possible for traders. +- Pools detail page, Remove pointless Add liquidity, Remove liquidity, Stake / claim buttons on pools details page, or better yet have them open appropriate modals from the manage liquidity section. +- Pools detail page, Incentives query degraded: REST smart query failed: 500 +- Pools detail page, Remove tags: Verified pool XYK No logo +- On pools detail page the activity table needs work. Pools / assets column shouldn't show long addresses or token factory address, just tickers. Conver ujuno to Juno, and make sure decimals are applied for numbers. The withdraws column shows USD unavailable, Fee unavailable. I should be easily able to copy the TX hash +- Hide the stats page for now. +- Navigation looks horrible on mobile, let's use best practices here. +- Select your wallet modal doesn't match theme. +- Right column on swap page needs real data. Real price chart, real recent transactions (last 10) +- Hide create page from nav for now. +- Make a SKILL.md file for agents trading on Juno DEX. +- Choose a different styling for the connect wallet button, it looks visually off. +- Only show "Portfolio" nav item if wallet is connected diff --git a/UX_AUDIT.md b/UX_AUDIT.md new file mode 100644 index 000000000..06424c576 --- /dev/null +++ b/UX_AUDIT.md @@ -0,0 +1,213 @@ +# JUNO DEX Frontend UX Audit + +Date: 2026-07-13 + +## Executive summary + +The frontend has a strong technical foundation: it avoids fabricated market data, simulates routes before enabling swaps, blocks the wrong network, acknowledges unverified routes, protects liquidity actions with minimums, handles first-provider risk carefully, and includes responsive layouts, focus-visible styles, reduced-motion support, and accessible shared modals. + +It is not ready for a public financial-product launch yet. Four issues are release blockers because they can mislead users or contribute to preventable loss: + +1. The default featured market is explicitly experimental but is inferred to be verified. +2. High-price-impact swaps remain executable without a dedicated warning or confirmation gate. +3. The UI calls reverse-simulated execution “exact output,” although the transaction does not guarantee exact output or cap maximum input. +4. Quote expiry is calculated but ignored, so a stale quote can remain actionable. + +The next tier of work is less about adding features and more about making each irreversible action understandable: show minimum received or maximum sent, fees, quote freshness, account, network, route, and risks in a review step; then provide one consistent transaction lifecycle with explorer access and safe recovery. After that, simplify technical copy, reduce degraded secondary-data noise, improve mobile chrome and touch targets, and raise the accessibility test bar. + +### Implementation update + +The four critical findings above have been addressed in the working tree: production routing now excludes non-active/unverified fixture markets, high and extreme price impact are gated, target-output language matches the bounded execution semantics, and expired quotes cannot proceed. Asset identity remains visible through review; live reserves inform route risk; explicitly blocked assets have no acknowledgement override. All write flows now use a shared pre-signature review and durable transaction lifecycle, and native/IBC/TokenFactory/CW20 paths are bounded or disabled when unsupported. + +Automated verification now passes 231 unit/component/integration tests across 46 files and 16 Playwright checks. The browser suite covers the mocked-wallet swap, add/remove liquidity, incentives, portfolio, and pool-creation journeys; wallet rejection, pre-hash broadcast failure, delayed-indexing timeout, duplicate-confirmation safety, stable quote transitions, mobile transaction access, and reduced-motion confirmation feedback; plus serious/critical WCAG 2.2 axe checks across core routes and transaction states, keyboard focus return, 320 CSS-pixel reflow, readable text, and mobile touch targets. Exact-message JUNO network-fee estimation is included in every transaction review. A 16-case execution matrix covers native, IBC, TokenFactory, and CW20 assets across direct and routed swaps, supported liquidity paths, and pool creation. Exact confirmed balance deltas reconcile immediately where safe. Pool details are organized around position management, risk, performance, and composition; secondary analytics failures degrade quietly; wallet, RPC/fallback, and indexer health are distinct; and compact mobile navigation keeps Portfolio and durable activity one tap away. The registry now exposes five active verified markets and passes its automated release check. Remaining launch work is tracked in `GOAL.md`, notably recorded market-provenance approval, manual assistive-technology and perceptual WCAG validation, moderated usability testing, complete matrix evidence, and owner sign-off. + +## Audit method + +Three parallel reviews covered: + +- Core journeys: swap, wallet connection, pool discovery, add/remove liquidity, portfolio, and transaction recovery. +- DeFi trust and safety: token identity, verification, price impact, slippage, quote freshness, fees, approvals, network state, and execution semantics. +- Interface quality: hierarchy, content, responsive behavior, accessibility, feedback, and visual delight. + +The review used source inspection, focused tests, the mocked E2E environment, and rendered checks at 1440 x 1000 and 390 x 844. This is a heuristic and implementation audit, not a substitute for moderated usability testing or a smart-contract/security audit. + +Severity means: + +- **Critical:** plausible asset loss or a materially misleading commitment. +- **High:** a major trust, completion, or recovery failure. +- **Medium:** meaningful friction, comprehension, accessibility, or polish debt. + +## What is working well + +- Routes are simulated and the swap action is disabled while a quote is updating or unavailable (`frontend/src/components/swap/SwapForm.tsx:67-107`). +- Wrong-network transactions are blocked and a network recovery banner exists (`frontend/src/components/wallet/NetworkGuardBanner.tsx`). +- Risk assessment and acknowledgement infrastructure already exists for pools, routes, and assets (`frontend/src/lib/risk.ts`). +- Initial liquidity has unusually good first-provider education and a typed `SEED` acknowledgement (`frontend/src/components/liquidity/AddLiquidityForm.tsx`). +- Withdrawals show expected and minimum assets under the current slippage bound (`frontend/src/components/liquidity/RemoveLiquidityForm.tsx`). +- Shared modals trap focus, support Escape, restore focus, and lock body scroll (`frontend/src/components/common/Modal.tsx`). +- The token selector supports search, favorites, recent tokens, balance display, identifiers, and risk badges (`frontend/src/components/swap/TokenSelect.tsx`). +- Responsive checks found no horizontal overflow at 390 px, and mobile pool rows restore labels when table headers disappear. +- The product is honest when optional data is absent; it does not invent chart, portfolio, or pool metrics. + +## Prioritized findings + +### Critical — release blockers + +#### 1. Experimental default market is presented as verified + +The first and featured registry pool is `JUNO / Juno Agent Test`. Its notes say it is experimental thin-liquidity infrastructure and “not a public launch market,” but it has no explicit `verified: false` (`frontend/src/data/registry.juno-1.json:15-39`). Risk logic treats every registry pool that is not explicitly false as verified (`frontend/src/lib/risk.ts:95-102`), and the swap asset list similarly inherits verification from registry provenance (`frontend/src/components/swap/SwapForm.tsx:28-39`). Registry notes are not used by the risk layer, and swap route risk receives no reserve data for thin-liquidity detection. + +**Impact:** a user sees a positive trust signal for exactly the market the registry warns operators not to launch publicly. + +**Recommendation:** make verification explicit and conservative. Unknown must mean unverified. Add lifecycle states such as `experimental`, `active`, `deprecated`, and `blocked`; exclude non-active pools from featured markets and default routing; feed reserves into route risk; surface relevant notes in plain language. + +#### 2. High-price-impact swaps have no loss-prevention gate + +Price impact of 5% or more is classified `high` (`frontend/src/lib/swap/slippage.ts:54-57`), but the prominent warning renders only for `warning`, not `high` (`frontend/src/components/swap/SwapForm.tsx:218-220`). Price impact is not part of submission validation. The current test explicitly expects a high-impact trade to stay enabled (`frontend/src/components/swap/SwapForm.test.tsx:198-210`). Multi-hop/router impact is shown as unavailable because router simulation values are overwritten with zero (`frontend/src/queries/useSwapQuote.ts:47-53`; `frontend/src/components/swap/QuoteCard.tsx:97-104`). + +**Impact:** the riskiest execution state gets less protective friction than the merely elevated state. + +**Recommendation:** always show impact. Require a dedicated acknowledgement and review step above a product-defined threshold; hard-block pathological impact unless a deliberately enabled advanced override is justified. Compute route-level impact or clearly disclose that it is unavailable and add protective friction. + +#### 3. Persisted slippage can reach 50% without being visible in the quote + +Custom slippage up to 50% is accepted and saved across sessions (`frontend/src/components/settings/SettingsPanel.tsx`; `frontend/src/settings/SlippageSettingsContext.tsx:23-36`). The quote replaces the effective value with three preset chips. If the stored value is custom, no chip is selected and the actual bound is not displayed (`frontend/src/components/swap/QuoteCard.tsx:106-131`). The settings entry point is icon-only. + +**Impact:** a returning user can unknowingly sign with an extreme execution tolerance. + +**Recommendation:** always render `Max slippage: X%` beside the quote and again in confirmation. Warn above a conservative threshold, require explicit confirmation for high values, and substantially reduce the maximum unless there is a proven need. + +#### 4. “Exact output” is not guaranteed by execution + +Editing the receive field performs a reverse simulation and labels the CTA `Swap exact output` (`frontend/src/components/swap/SwapForm.tsx:62-70,108-126`). Execution then submits a normal exact-input swap using the simulated input. For router routes, `minimum_receive` is below the requested output; there is no maximum-input bound (`frontend/src/components/swap/SwapForm.tsx:143-154`; `frontend/src/mutations/useSwapTx.ts:29-41`). + +**Impact:** the UI promises a transaction property it does not enforce. + +**Recommendation:** implement genuine exact-output semantics with maximum-input protection, or rename the mode to `Target output estimate`, show that output may vary, and show the actual minimum received. Do not call the current behavior exact output. + +### High — complete before public launch + +#### 5. Quote freshness is computed but not enforced + +The quote hook calculates age, expiration, and a 30-second TTL (`frontend/src/queries/useSwapQuote.ts:98-113`). `SwapForm` does not check `isExpired`. It passes `updatedAt` to `QuoteCard`, but the card does not display or otherwise use it (`frontend/src/components/swap/SwapForm.tsx:217`; `frontend/src/components/swap/QuoteCard.tsx:14-31`). + +**Recommendation:** show an unobtrusive freshness/countdown state, disable expired quotes, refresh immediately before review, and bind the review to the exact route and quote being signed. + +#### 6. Irreversible actions have no app-level review step + +Swap, add liquidity, and remove liquidity broadcast directly from their primary CTA. Wallets may show raw CosmWasm messages, so the app is currently missing the human-readable commitment layer. + +**Recommendation:** use one reusable review sheet for every write action. At minimum show: + +- action and human-formatted amounts; +- minimum received or maximum sent; +- connected account and `juno-1`; +- token/pool verification and warnings; +- route and contract disclosure; +- LP/protocol fee and estimated network fee; +- quote age and expiry; +- clear `Confirm in wallet` final action. + +#### 7. Swap quote omits key decision information + +The quote shows rate, route, price impact, and slippage only (`frontend/src/components/swap/QuoteCard.tsx:84-134`). It omits minimum received, commission/LP fee even though direct quotes contain it, and estimated gas/network fee. Router impact and commission are unavailable by construction. + +This falls short of the current established swap pattern, which exposes fee, network cost, route, price impact, slippage, and minimum output before commitment. + +**Recommendation:** expose net outcome first, then progressive detail. Show minimum received/max input, route fee breakdown, network fee in JUNO (and fiat when reliable), and route-level impact. + +#### 8. Transaction status and recovery are inconsistent + +Transaction state infrastructure exists, but only create-pool renders `TxStatusDialog`. Swap/add/remove rely on fragmented inline text and toasts. The runner defines preparing, signing, and broadcasting states but jumps from signing to success/failure (`frontend/src/tx/useTxRunner.tsx`). Hashes are raw code rather than explorer links. Remove liquidity adds a second toast layer, risking duplicate notices. Timeout retry can blindly rebroadcast non-idempotent variables. + +**Recommendation:** provide one persistent lifecycle: review -> confirm in wallet -> submitted -> confirmed/failed. Link every hash to the configured explorer, persist status across navigation, and check transaction/account state before offering a retry after timeout. + +#### 9. Primary connect and network-recovery CTAs are inert + +The swap action says `Connect wallet to swap` or `Switch to Juno to swap`, but the button is disabled in those states (`frontend/src/components/swap/SwapForm.tsx:103-116,224`). Remove liquidity behaves similarly. Add liquidity contains connect/switch branches, but disabling can make the wrong-network branch unreachable. + +**Recommendation:** let the primary action connect or switch networks, preserve the user's entered intent, then revalidate before review. Reserve disabled buttons for invalid amounts, unavailable quotes, and pending actions. + +#### 10. Token identity and status disappear after selection + +The swap's compact selected-token controls hide identifiers and verification state (`frontend/src/components/swap/SwapForm.tsx:180-213`). Risk badges appear inside the selector, but not at commitment time. The known-bad denom list is empty, and its future `denylisted` state would still permit acknowledgement rather than block execution (`frontend/src/lib/risk.ts:23-26,71-73`). + +**Recommendation:** keep verified/unverified status visible beside selected tokens; provide a concise denom/contract disclosure with copy and explorer actions; distinguish native, IBC, TokenFactory, and CW20 assets. Block known malicious assets; acknowledgement is for unknown assets, not known-bad ones. + +#### 11. Network health can be falsely reassuring + +The sidebar always says `juno-1`, `Live`, and `Phase Δ.4.0.0` (`frontend/src/components/layout/DexShell.tsx:83-87`), even while RPC or indexer-backed areas are degraded. + +**Recommendation:** separate wallet chain, RPC health/sync, and indexer freshness. Derive health from real checks and timestamp it. Move build/phase metadata into diagnostics. + +#### 12. CW20 support is exposed without a complete transaction UX + +The asset model accepts CW20s, but current swap and liquidity broadcasts use direct contract execution/native funds paths without an allowance/send/approval flow. There is no spender, approval amount, multi-transaction progress, or revoke guidance. + +**Recommendation:** do not expose unsupported CW20 actions. Validate and implement the correct atomic send hook where available. If approval is required, request an exact amount and clearly disclose token, spender, amount, transaction count, allowance state, and revoke path; never default to unlimited approval. + +### Medium — comprehension, accessibility, and delight + +#### 13. The core swap starts with an unsolicited amount and lacks useful balance actions + +The send amount initializes to `1`, immediately causing quote work (`frontend/src/components/swap/SwapForm.tsx:52`). Both swap fields hide existing Half/MAX controls; the receive-token balance is not passed, so it can show `bal —`. An undefined/loading balance is treated as not exceeding the balance. + +**Recommendation:** start empty; show both balances; add gas-aware MAX for native JUNO plus a 50% shortcut; keep the previous quote dimmed while refreshing to avoid layout shifts; do not enable execution until the relevant balance state is known. + +#### 14. Optional-data failures make the entire product feel broken + +When the indexer is degraded, the secondary chart and recent-activity panel can dominate the swap page with unavailable/error copy even though swapping still works (`frontend/src/components/swap/SwapPage.tsx:31-54`). + +**Recommendation:** use stale cached data with a timestamp when possible. Otherwise collapse optional panels into a quiet compact placeholder. Reserve prominent red errors for trade-blocking failures. + +#### 15. Content often speaks to operators rather than traders + +Messages such as `strict registry`, `factory discovery`, `no fake rows`, `indexer request failed`, and implementation explanations on pool detail pages expose internal architecture. Pool details mix decision information with contract identity, share math, query status, and unsupported-parameter commentary. + +**Recommendation:** lead with user outcomes and available actions. Put technical diagnostics and raw identifiers behind `Details`. Structure pool details around performance, composition, the user's position, risks, and manage-liquidity actions. + +#### 16. Accessibility coverage and readable contrast are below the desired bar + +`textSubtle` (`#6E5C4A`) measures approximately 2.94:1 on card backgrounds and is widely used at 0.58-0.72rem (`frontend/src/theme/junoTheme.ts:13-16`; `frontend/src/styles/theme.css`). The automated accessibility suite checks WCAG 2.0/2.1 tags and rejects only `critical` axe findings, allowing `serious` findings to pass (`frontend/e2e/a11y.spec.ts:4-13,28-34`). + +Other gaps include no skip link or SPA route focus/title management, sort controls without `aria-sort`, and charts that can expose one tab stop per candle. + +**Recommendation:** meet WCAG 2.2 AA; bring all informative text to at least 4.5:1; use a readable minimum text size; fail CI on serious and critical findings; add mobile, keyboard, focus-order, zoom/reflow, dialog, and transaction-state tests. Use one composite chart focus target plus a summary/table alternative. + +#### 17. Mobile works, but it is not yet effortless + +At <=860 px, the brand/hamburger row is followed by a separate wallet-only row, consuming roughly 114 px before content (`frontend/src/styles/theme.css:2229-2280`). Settings, flip, close, and slippage controls use small 34 px or compact targets. Settings uses `role=dialog` without the robust keyboard behavior of the shared modal. Persistent toasts can obscure bottom content. + +**Recommendation:** use one sticky app bar with a compact account control; evaluate bottom navigation for Swap/Pools/Portfolio; make core touch targets approximately 44 px; reuse the shared modal/popover behavior; make toasts time-limited with pause-on-hover/focus and back them with a persistent transaction center. + +#### 18. Wallet and pool-management entry points need clearer intent + +Clicking a connected wallet immediately disconnects, with no account menu, address copy, explorer link, or switch-wallet action. On the pools page, `+ Provide` leads to pool creation rather than providing liquidity. The legacy liquidity overview always displays an empty-state card even when positions may be present. + +**Recommendation:** open an account sheet and make disconnect an explicit secondary action. Rename `Provide` to `Create pool`, or route it to a genuine add-liquidity flow. Show liquidity helper/empty content only when it matches the user's actual state. + +## Baseline validation observations + +- Rendered desktop and 390 px mobile checks found no horizontal overflow. +- At audit time, the automated accessibility run asserted only the absence of `critical` axe violations; that was not evidence of WCAG 2.2 AA conformance. The implementation update above records the stronger current automated gate, while `ACCESSIBILITY_AUDIT.md` lists the manual evidence still required. +- At audit time, the focused swap and liquidity E2E expectations were stale relative to the UI. Those journeys and their recovery states now pass in the current 15-check browser suite. + +## Recommended sequence + +1. Correct verification defaults and remove experimental pools from public defaults. +2. Make execution semantics truthful; add price-impact, slippage, and stale-quote gates. +3. Add complete quote disclosure and a shared review step. +4. Unify transaction status, explorer links, and timeout recovery. +5. Repair connect/switch actions, balance loading, and token identity. +6. Simplify content and degraded optional panels. +7. Complete WCAG 2.2 AA/mobile work and repair critical-flow E2E coverage. +8. Add measured delight: stable quote transitions, restrained success motion, useful freshness cues, and fast account/portfolio access. + +## External benchmark references + +- [Uniswap Web App: The Swap Screen](https://support.uniswap.org/hc/en-us/articles/39862756339341-Uniswap-Web-App-The-Swap-Screen) — current swap-detail conventions including fees, network cost, routing, impact, slippage, and minimum output. +- [Uniswap: What is price impact?](https://support.uniswap.org/hc/en-us/articles/8671539602317-What-is-Price-Impact) — high-impact warning and deliberate override pattern. +- [Uniswap: Price impact vs. price slippage](https://support.uniswap.org/hc/en-us/articles/8643794102669-Price-Impact-vs-Price-Slippage) — language for distinguishing two frequently confused concepts. +- [Uniswap: What are token warnings?](https://support.uniswap.org/hc/en-us/articles/40074236290445-What-are-token-warnings) — persistent token decision-support patterns. +- [WCAG 2.2](https://www.w3.org/TR/WCAG22/) — contrast, reflow, focus, status, target-size, and error-prevention requirements. +- [Nielsen Norman Group usability heuristics](https://media.nngroup.com/media/articles/attachments/Heuristic_Summary1_A4_compressed.pdf) — system visibility, user control, error prevention, consistency, and recovery principles. diff --git a/audit/diff-a-keep-set-changes.patch b/audit/diff-a-keep-set-changes.patch index 7b2b8173c..97331f4e7 100644 --- a/audit/diff-a-keep-set-changes.patch +++ b/audit/diff-a-keep-set-changes.patch @@ -3212,7 +3212,7 @@ index 1d8a1e9f..00000000 +++ /dev/null @@ -1,28 +0,0 @@ -{ -- "private_key": "8482bce4e5f250bb775f788ce89abc4717980e97c618a1f26278b195b3b6b05f", +- "private_key": "", - "public_key": "0229fc5e5a420a15020e62cde603b5285f4908918f27632daf85435f3ab9d293d5", - "address": [ - 14, @@ -3247,7 +3247,7 @@ index e43dfc9e..00000000 +++ /dev/null @@ -1,28 +0,0 @@ -{ -- "private_key": "f5cbe80991c82afe42d51e2dc9946332c4322aff69cbc23fc25a4f05a73eb419", +- "private_key": "", - "public_key": "0316b8bfc0d651848fdab2dd4befdd86bbf89bf37251ee2f68789586284e53c993", - "address": [ - 176, @@ -10366,15 +10366,15 @@ index 7c6aa0f5..00000000 - -export const LCD = extendLCD(new LCDClient(CHAINS)) - --export const USER_MNEMONIC = "journey proud segment gorilla pencil common phone cloth undo walk civil add gate six measure often addict turn because wet bachelor mechanic ozone early" +REDACTED_WALLET_SECRET_PLACEHOLDER= - -export type Signer = { signer: Wallet, address: string, chain_id: string } - -export const get_signers = (): Record => { -- const terra_signer = LCD.wallet(new MnemonicKey({mnemonic: USER_MNEMONIC, coinType: 330})); +REDACTED_WALLET_SECRET_PLACEHOLDER= - const terra_signer_addr = terra_signer.key.accAddress("terra") - -- const neutron_signer = LCD.wallet(new MnemonicKey({mnemonic: USER_MNEMONIC, coinType: 118})); +REDACTED_WALLET_SECRET_PLACEHOLDER= - const neutron_signer_addr = neutron_signer.key.accAddress("neutron") - - return { diff --git a/contracts/tokenomics/incentives/src/reply.rs b/contracts/tokenomics/incentives/src/reply.rs index 8eaac4b44..c869678e3 100644 --- a/contracts/tokenomics/incentives/src/reply.rs +++ b/contracts/tokenomics/incentives/src/reply.rs @@ -72,8 +72,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result= FIRST_DYNAMIC_REPLY_ID => { - let Some((reward_key, amount)) = - PENDING_REWARD_TRANSFERS.may_load(deps.storage, id)? + let Some((reward_key, amount)) = PENDING_REWARD_TRANSFERS.may_load(deps.storage, id)? else { // Defensive: a payload we did not register. Best-effort log and // continue rather than aborting the whole transaction. diff --git a/contracts/tokenomics/incentives/src/state.rs b/contracts/tokenomics/incentives/src/state.rs index 98075777a..18869dc10 100644 --- a/contracts/tokenomics/incentives/src/state.rs +++ b/contracts/tokenomics/incentives/src/state.rs @@ -89,6 +89,11 @@ pub struct PoolInfo { /// NOTE: this is not part of serialized structure in state! #[serde(skip)] pub rewards_to_remove: HashMap, + /// Active rewards accrued while no LP tokens were staked and later made + /// recoverable through [`ORPHANED_REWARDS`]. This is in-memory only and is + /// flushed by [`PoolInfo::save`]. + #[serde(skip)] + pub orphaned_rewards_to_claim: HashMap, } impl PoolInfo { @@ -169,10 +174,15 @@ impl PoolInfo { if self.total_lp.is_zero() { reward_info.orphaned += collected_rewards; } else { - // Allowing the first depositor to claim orphaned rewards - reward_info.index += (reward_info.orphaned + collected_rewards) - / Decimal256::from_ratio(self.total_lp, 1u8); - reward_info.orphaned = Decimal256::zero(); + if !reward_info.orphaned.is_zero() { + self.orphaned_rewards_to_claim + .entry(reward_info.reward.asset_info().clone()) + .and_modify(|amount| *amount += reward_info.orphaned) + .or_insert(reward_info.orphaned); + reward_info.orphaned = Decimal256::zero(); + } + + reward_info.index += collected_rewards / Decimal256::from_ratio(self.total_lp, 1u8); } if need_remove { @@ -445,6 +455,19 @@ impl PoolInfo { /// If reward schedule has orphaned rewards accumulate them in ORPHANED_REWARDS. /// This function consumes self just to make sure it becomes unusable after calling save(). pub fn save(self, storage: &mut dyn Storage, lp_token: &AssetInfo) -> StdResult<()> { + for (reward, orphaned_amount) in &self.orphaned_rewards_to_claim { + if !orphaned_amount.is_zero() { + ORPHANED_REWARDS.update::<_, StdError>( + storage, + &asset_info_key(reward), + |amount| { + Ok(amount.unwrap_or_default() + + Uint128::try_from(orphaned_amount.to_uint_floor())?) + }, + )?; + } + } + if !self.rewards_to_remove.is_empty() { self.rewards_to_remove .iter() @@ -499,7 +522,10 @@ pub fn list_pool_stakers( limit: Option, ) -> StdResult> { let start = start_after.as_ref().map(Bound::exclusive); - let limit = limit.unwrap_or(MAX_PAGE_LIMIT).max(MAX_PAGE_LIMIT); + let limit = limit.unwrap_or(MAX_PAGE_LIMIT).min(MAX_PAGE_LIMIT); + if limit == 0 { + return Err(StdError::generic_err("limit must be > 0")); + } USER_INFO .prefix(lp_token) .range(storage, start, None, Order::Ascending) @@ -714,3 +740,54 @@ impl UserInfo { USER_INFO.remove(storage, (lp_token, user)) } } + +#[cfg(test)] +mod tests { + use astroport::asset::AssetInfo; + use astroport::incentives::MAX_PAGE_LIMIT; + use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::Uint128; + + use super::{list_pool_stakers, UserInfo, USER_INFO}; + + #[test] + fn list_pool_stakers_caps_requested_limit_to_max_page_limit() { + let mut deps = mock_dependencies(); + let lp_token = AssetInfo::native("factory/juno1pool/ulp"); + + for index in 0..(MAX_PAGE_LIMIT + 5) { + let user = deps.api.addr_make(&format!("staker{index:02}")); + USER_INFO + .save( + deps.as_mut().storage, + (&lp_token, &user), + &UserInfo { + amount: Uint128::new(index as u128 + 1), + last_rewards_index: vec![], + last_claim_time: 0, + }, + ) + .unwrap(); + } + + let stakers = list_pool_stakers( + deps.as_ref().storage, + &lp_token, + None, + Some(MAX_PAGE_LIMIT + 5), + ) + .unwrap(); + + assert_eq!(stakers.len(), MAX_PAGE_LIMIT as usize); + } + + #[test] + fn list_pool_stakers_rejects_zero_limit() { + let deps = mock_dependencies(); + let lp_token = AssetInfo::native("factory/juno1pool/ulp"); + + let err = list_pool_stakers(deps.as_ref().storage, &lp_token, None, Some(0)).unwrap_err(); + + assert_eq!(err.to_string(), "Generic error: limit must be > 0"); + } +} diff --git a/contracts/tokenomics/incentives/src/utils.rs b/contracts/tokenomics/incentives/src/utils.rs index 2721f7e1d..8a41e7439 100644 --- a/contracts/tokenomics/incentives/src/utils.rs +++ b/contracts/tokenomics/incentives/src/utils.rs @@ -903,7 +903,7 @@ mod unit_tests { // Seed an "active" PoolInfo — non-zero internal rps so // `is_active_pool()` returns true and the deactivate_pool match // arm enters the `Some(_) if pool_info.is_active_pool()` branch. - let mut pool_info = PoolInfo { + let pool_info = PoolInfo { total_lp: Uint128::zero(), rewards: vec![RewardInfo { reward: RewardType::Int(reward_token.clone()), @@ -913,6 +913,7 @@ mod unit_tests { }], last_update_ts: env.block.time.seconds(), rewards_to_remove: Default::default(), + orphaned_rewards_to_claim: Default::default(), }; pool_info.save(deps.as_mut().storage, &lp_asset).unwrap(); diff --git a/deployment/MAINNET_DEPLOYMENT.md b/deployment/MAINNET_DEPLOYMENT.md new file mode 100644 index 000000000..55da32fb8 --- /dev/null +++ b/deployment/MAINNET_DEPLOYMENT.md @@ -0,0 +1,326 @@ +# Astroport-Juno v1 mainnet deployment guide + +Scope: Juno mainnet (`juno-1`) deployment of the stripped Astroport-Juno v1 DEX surface. + +This guide is an operator runbook. It does not authorize broadcasting transactions by itself. Broadcast only after the owner/guardian/treasury/uploader/counterparty/seed-liquidity decisions are explicitly approved. + +## 0. Launch constraints + +- Chain: `juno-1` +- Native denom: `ujuno` +- Product scope: XYK swaps, pools, and liquidity only. +- No DEX token, stablecoin, LST, perps, yield vaults, PCL/stable pairs, maker, staking, vesting, converter, or xASTRO launch surface. +- Frontend launch is blocked until at least one seeded XYK pool verifies via factory `pairs`, pair `pool`, and pair `simulation` queries. + +## 1. Human decisions required before broadcast + +Fill this table before any `junod tx ... --yes` command is run. + +| Decision | Value | Source / approval | +|---|---|---| +| Upload signer key name | `TODO` | `TODO` | +| DAO/steward owner/admin | `TODO` | `TODO` | +| Incentives guardian | `TODO` | `TODO` | +| Treasury / fee destination | `TODO` | `TODO` | +| First counterparty denom | `TODO` | `TODO` | +| Counterparty decimals | `TODO` | `TODO` | +| Seed liquidity plan | `TODO` | `TODO` | +| Public launch comms owner | `TODO` | `TODO` | + +Recommended default for owner/guardian/treasury is a DAO-controlled address, not an unattended hot wallet. If a hot wallet must deploy, transfer ownership/admin controls immediately after verification. + +## 2. Environment + +```sh +export CHAIN_ID=juno-1 +export DENOM=ujuno +export RPC=https://juno-rpc.publicnode.com:443 +export REST=https://juno-rest.publicnode.com +export KEY_NAME=juno-agent # replace if a different approved uploader is used +export KEYRING_DIR=/opt/data/.juno-agent +export KEYRING_BACKEND=test +export GAS_PRICES=0.075ujuno +export JUNOD=/opt/data/bin/junod +``` + +Never print or export private keys or seed phrases. Keep tx JSON outputs; they are the deployment evidence. + +## 3. Preflight: re-query mainnet state + +Run immediately before any tx work: + +```sh +$JUNOD version +curl -fsS "$RPC/status" | jq -r '.result.node_info.network, .result.sync_info.catching_up, .result.sync_info.latest_block_height' +curl -fsS "$REST/cosmos/base/tendermint/v1beta1/node_info" | jq -r '.default_node_info.network' +$JUNOD query wasm params --node "$RPC" -o json | jq +$JUNOD query globalfee minimum-gas-prices --node "$RPC" -o json | jq +$JUNOD query auth module-account tokenfactory --node "$RPC" -o json | jq -r '.account.value.address // .account.address' +$JUNOD keys show "$KEY_NAME" --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" -a +$JUNOD query bank balances "$($JUNOD keys show "$KEY_NAME" --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" -a)" --node "$RPC" -o json | jq +``` + +Abort if: + +- RPC/REST network is not `juno-1`. +- Node is catching up. +- Wasm upload/instantiate params do not permit the chosen path. +- Gas price policy differs materially from `0.075ujuno`. +- Uploader does not have enough `ujuno` for stores, instantiates, pool creation, and smoke tests. + +## 4. Build optimized artifacts + +Use a machine with Docker daemon access or an equivalent reproducible CosmWasm optimizer path. + +```sh +cd /opt/data/repos/astroport-core +scripts/build_release.sh +python3 scripts/check_juno_v1_artifacts.py artifacts +``` + +The artifact set must be exactly: + +```text +artifacts/astroport_factory.wasm +artifacts/astroport_pair.wasm +artifacts/astroport_router.wasm +artifacts/astroport_native_coin_registry.wasm +artifacts/astroport_oracle.wasm +artifacts/astroport_tokenfactory_tracker.wasm +artifacts/astroport_whitelist.wasm +artifacts/astroport_incentives.wasm +``` + +Then run `cosmwasm-check` on each artifact: + +```sh +for wasm in \ + artifacts/astroport_factory.wasm \ + artifacts/astroport_pair.wasm \ + artifacts/astroport_router.wasm \ + artifacts/astroport_native_coin_registry.wasm \ + artifacts/astroport_oracle.wasm \ + artifacts/astroport_tokenfactory_tracker.wasm \ + artifacts/astroport_whitelist.wasm \ + artifacts/astroport_incentives.wasm; do + cosmwasm-check --available-capabilities staking,cosmwasm_1_1,cosmwasm_2_0,iterator,stargate "$wasm" +done +``` + +## 5. Upload contracts and capture code IDs + +Create a durable tx output directory: + +```sh +mkdir -p deployment/tx/juno-1 +``` + +For each v1 wasm: + +```sh +SYNC_JSON=/tmp/store-astroport-factory.sync.json +$JUNOD tx wasm store artifacts/astroport_factory.wasm \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.5 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --broadcast-mode sync --yes -o json \ + > "$SYNC_JSON" +TXHASH=$(jq -r '.txhash' "$SYNC_JSON") +test -n "$TXHASH" && test "$TXHASH" != null +# Wait for inclusion, then save the included tx response with DeliverTx events. +until $JUNOD query tx "$TXHASH" --node "$RPC" -o json > deployment/tx/juno-1/store-astroport-factory.json; do sleep 3; done +``` + +Do not feed the sync broadcast JSON directly to `extract_juno_v1_tx_sets.py`: sync responses generally only contain CheckTx/txhash. Always wait for inclusion and save the included tx response from `junod query tx -o json` (or an equivalent full tx response containing DeliverTx events). + +Repeat for: + +- `astroport-incentives` +- `astroport-native-coin-registry` +- `astroport-oracle` +- `astroport-pair` +- `astroport-router` +- `astroport-tokenfactory-tracker` +- `astroport-whitelist` + +Also provide `cw20-base` code ID. Either use a verified existing mainnet code ID or upload a pinned `cw20-base` artifact and save `deployment/tx/juno-1/store-cw20-base.json`. + +Extract/check code IDs: + +```sh +python3 scripts/extract_juno_v1_tx_sets.py --scan deployment/tx/juno-1/store-astroport-factory.json +``` + +## 6. Instantiate order + +Use the update-after-incentives path unless the team explicitly chooses Instantiate2 and records salt/checksum/predicted-address evidence. + +1. Instantiate native coin registry. +2. Register `ujuno` and the verified first counterparty denom + decimals. +3. Instantiate whitelist. +4. Instantiate factory with one XYK pair config, `permissioned=true`, and `generator_address=null`. Keep XYK pair creation permissioned until the official first pair exists and seed liquidity is confirmed. +5. Instantiate incentives with factory address, owner, guardian, and `reward_token={"native_token":{"denom":"ujuno"}}`. +6. Execute factory `update_config` to set `generator_address` to incentives. +7. Instantiate router with factory address. +8. Instantiate oracle only after a real pair asset vector exists, or keep oracle dormant/out of the frontend launch-critical path. +9. Instantiate standalone tokenfactory tracker only if the operator still needs it; factory-created pair trackers are the critical path. +10. Verify the official first pair does not already exist by querying factory `pair`/`pairs` for the launch asset infos. If it exists unexpectedly, stop and reconcile before continuing. +11. Create the official first XYK pair through factory from the approved owner/operator wallet. +12. Immediately seed official liquidity from the approved seed wallet. +13. Query factory pair registry and pool balances; require the official pair address to be registered and pool liquidity to be non-zero. +14. Run the smoke checks in section 8, then execute factory `update_pair_config` for XYK with the same fees/code ID and `permissioned=false` to open public pair creation. + +Save every tx response under `deployment/tx/juno-1/` with explicit names, for example: + +```text +instantiate-astroport-native-coin-registry.json +execute-native-coin-registry-add-ujuno.json +execute-native-coin-registry-add-counterparty.json +instantiate-astroport-whitelist.json +instantiate-astroport-factory.json +instantiate-astroport-incentives.json +execute-factory-update-generator.json +instantiate-astroport-router.json +instantiate-astroport-oracle.json +instantiate-astroport-tokenfactory-tracker.json +execute-factory-create-pair.json +execute-pair-provide-liquidity.json +execute-factory-open-public-pair-creation.json +``` + +## 7. Render deployment config from tx output + +Build a tx set, then render the canonical handoff: + +```sh +python3 scripts/extract_juno_v1_tx_sets.py \ + --code-id astroport-factory=deployment/tx/juno-1/store-astroport-factory.json \ + --code-id astroport-incentives=deployment/tx/juno-1/store-astroport-incentives.json \ + --code-id astroport-native-coin-registry=deployment/tx/juno-1/store-astroport-native-coin-registry.json \ + --code-id astroport-oracle=deployment/tx/juno-1/store-astroport-oracle.json \ + --code-id astroport-pair=deployment/tx/juno-1/store-astroport-pair.json \ + --code-id astroport-router=deployment/tx/juno-1/store-astroport-router.json \ + --code-id astroport-tokenfactory-tracker=deployment/tx/juno-1/store-astroport-tokenfactory-tracker.json \ + --code-id astroport-whitelist=deployment/tx/juno-1/store-astroport-whitelist.json \ + --code-id cw20-base=deployment/tx/juno-1/store-cw20-base.json \ + --address astroport-factory=deployment/tx/juno-1/instantiate-astroport-factory.json \ + --address astroport-incentives=deployment/tx/juno-1/instantiate-astroport-incentives.json \ + --address astroport-native-coin-registry=deployment/tx/juno-1/instantiate-astroport-native-coin-registry.json \ + --address astroport-oracle=deployment/tx/juno-1/instantiate-astroport-oracle.json \ + --address astroport-router=deployment/tx/juno-1/instantiate-astroport-router.json \ + --address astroport-tokenfactory-tracker=deployment/tx/juno-1/instantiate-astroport-tokenfactory-tracker.json \ + --address astroport-whitelist=deployment/tx/juno-1/instantiate-astroport-whitelist.json \ + > deployment/tx/juno-1/tx-sets.txt + +python3 scripts/build_juno_v1_deployment_command.py \ + --tx-sets deployment/tx/juno-1/tx-sets.txt \ + --network juno-1 \ + --owner "$JUNO_OWNER" \ + --guardian "$JUNO_GUARDIAN" \ + --treasury "$JUNO_TREASURY" \ + --tokenfactory-module "$JUNO_TOKENFACTORY_MODULE" \ + --counterparty-denom "$FIRST_COUNTERPARTY_DENOM" \ + --output deployment/juno-v1-mainnet.json \ + --render +``` + +Validate: + +```sh +python3 scripts/check_juno_v1_deployment_template.py deployment/juno-v1-mainnet.json +python3 scripts/check_juno_v1_frontend_config.py deployment/juno-v1-mainnet.json +python3 -m json.tool deployment/juno-v1-mainnet.json >/tmp/juno-v1-mainnet.pretty.json +``` + +## 8. First-pool gate and post-deploy verification queries + +Keep XYK pair creation permissioned until all first-pool gate evidence below is captured. Do not open `permissioned=false` while the factory has no official seeded pair. + +```sh +$JUNOD query wasm contract-state smart "$ADDR_FACTORY" '{"config":{}}' --node "$RPC" -o json | jq +$JUNOD query wasm contract-state smart "$ADDR_NATIVE_COIN_REGISTRY" '{"native_tokens":{"limit":30}}' --node "$RPC" -o json | jq +$JUNOD query wasm contract-state smart "$ADDR_FACTORY" '{"pair":{"asset_infos":[{"native_token":{"denom":"ujuno"}},{"native_token":{"denom":"'$FIRST_COUNTERPARTY_DENOM'"}}]}}' --node "$RPC" -o json | jq +$JUNOD query wasm contract-state smart "$ADDR_FACTORY" '{"pairs":{"limit":30}}' --node "$RPC" -o json | jq +$JUNOD query wasm contract-state smart "$PAIR_ADDR" '{"pool":{}}' --node "$RPC" -o json | jq +$JUNOD query wasm contract-state smart "$PAIR_ADDR" '{"simulation":{"offer_asset":{"info":{"native_token":{"denom":"ujuno"}},"amount":"1000000"}}}' --node "$RPC" -o json | jq +$JUNOD query wasm contract-state smart "$ADDR_ROUTER" '{"config":{}}' --node "$RPC" -o json | jq +``` + +Only after the official pair query resolves to the expected `$PAIR_ADDR`, pool balances are non-zero, and smoke checks pass, open public creation: + +```sh +jq '.post_update_state["astroport-factory"].pair_configs[0] | {update_pair_config:{config:.}}' deployment/juno-v1-mainnet.json > /tmp/open-public-pair-creation-msg.json +$JUNOD tx wasm execute "$ADDR_FACTORY" "$(cat /tmp/open-public-pair-creation-msg.json)" \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.4 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" +``` + +Expected launch state: + +- Factory owner is the approved DAO/steward owner. +- Factory has exactly one active XYK pair config. It remains `permissioned=true` until the official pair is seeded and verified, then becomes `permissioned=false` only after the open-public-creation transaction. +- Native registry returns correct decimals for `ujuno` and first counterparty denom. +- Factory `pairs` includes the first seeded pair. +- Pair `pool` returns non-zero liquidity. +- Pair `simulation` returns a sane ask amount. +- Router config points to the deployed factory. + +## 9. Frontend registry handoff + +Only after section 8 passes, fill the DEX frontend registry with real values: + +- chain ID: `juno-1` +- native denom: `ujuno` +- working RPC/REST endpoints +- factory address +- native coin registry address +- router address +- incentives address +- optional oracle address +- first pool metadata if the frontend expects static bootstrap metadata + +Then run the frontend repo checks: + +```sh +cd /opt/data/repos/juno-website-dex-v1 +python3 -m json.tool public/dex/registry.juno-1.json >/tmp/registry.juno-1.pretty.json +yarn lint +yarn type-check +yarn build +``` + +## 10. Smoke tests before public announcement + +Minimum live smoke: + +1. Direct swap using the seeded pair. +2. Add liquidity. +3. Remove a small amount of liquidity. +4. Query balances before/after each action. +5. Verify explorer links and contract labels. +6. Publish a risk notice: experimental v1 DEX, thin liquidity, verify contracts. + +## 11. Freeze / rollback playbook + +- Bad code ID: stop using it, upload fixed code, instantiate replacements or migrate only if migration is explicitly safe. +- Bad factory config: execute factory update to disable XYK pair config or remove the factory from the frontend registry. +- Bad incentives config: hide incentives in UI and set factory `generator_address=null` if needed. +- Bad pool: remove from frontend registry, warn publicly, create a replacement pool, do not seed more liquidity. +- Bad denom decimals: freeze affected denom in frontend/native registry until fixed and reverified. +- Router issue: hide multi-hop/router paths; direct pair swaps remain the launch-critical surface. + +## 12. Final launch gate + +Do not call launch ready until all are true: + +- [ ] Human decision table is filled and approved. +- [ ] Optimized artifact set is exact and checked. +- [ ] Code IDs and contract addresses are captured from real tx JSON. +- [ ] Owner/guardian/treasury are correct on-chain. +- [ ] First counterparty denom trace + decimals are verified. +- [ ] First XYK pool exists and has seeded liquidity. +- [ ] Factory `pairs`, pair `pool`, and pair `simulation` queries pass. +- [ ] Frontend registry has no placeholders and builds. +- [ ] Swap + add/remove liquidity smoke tests pass. +- [ ] Risk notice and contract address list are ready for public comms. diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 000000000..cbd50cfd0 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,130 @@ +# Astroport-Juno v1 deployment handoff + +This folder is the narrow handoff between contract upload/instantiate output and the frontend config for the uni-7 bakeoff. + +## Files + +- `juno-v1-testnet.template.json` — canonical placeholder template for the v1 surface. +- `juno-v1-testnet.json` — suggested rendered output path; do not commit real values until the DAO/stewards choose to publish them. +- `juno-v1-mainnet.json` — rendered mainnet output path; do not commit real values until the DAO/stewards choose to publish them. +- `juno-v1-readiness-plan.md` — operator deployment/readiness plan with instantiate order, no-broadcast dry-run commands, safety checks, rollback/freeze risks, and exact blockers. +- `MAINNET_DEPLOYMENT.md` — mainnet `juno-1` operator runbook for approvals, artifact checks, tx capture, config rendering, frontend handoff, smoke tests, and rollback/freeze actions. +- `frontend-release-checklist.md` — final copy/verification checklist for moving the rendered handoff into the UI repo. + +## Required values after upload / instantiate + +Collect these from the real uni-7 transaction output before rendering: + +### Accounts + +- `accounts.owner` — DAO/steward admin for owned contracts. +- `accounts.guardian` — incentives guardian. +- `accounts.treasury` — fee destination and incentives vesting placeholder for v1 native rewards. +- `accounts.tokenfactory_module` — chain tokenfactory module address used by tracker/factory config. + +### Code IDs + +- `code_ids.astroport-factory` +- `code_ids.astroport-incentives` +- `code_ids.astroport-native-coin-registry` +- `code_ids.astroport-oracle` +- `code_ids.astroport-pair` +- `code_ids.astroport-router` +- `code_ids.astroport-tokenfactory-tracker` +- `code_ids.astroport-whitelist` +- `code_ids.cw20-base` + +### Instantiated addresses + +- `addresses.astroport-factory` +- `addresses.astroport-incentives` +- `addresses.astroport-native-coin-registry` +- `addresses.astroport-oracle` +- `addresses.astroport-router` +- `addresses.astroport-tokenfactory-tracker` +- `addresses.astroport-whitelist` + +### First pool counterpart denom + +- `pair_create_msg_template.asset_infos.1.native_token.denom` — real `ibc/...` denom for the non-`ujunox` side of the first test pool. + +## Extract values from tx JSON + +For the complete operator handoff, use [`operator-tx-checklist.md`](operator-tx-checklist.md). It names the 16 expected `junod -o json` tx files, builds `deployment/tx/uni-7/tx-sets.txt`, and feeds the deployment command builder. + +To rehearse the full handoff without chain txs, generate harmless synthetic fixtures in an ignored directory: + +```sh +python3 scripts/generate_juno_v1_dry_run_txs.py --output-dir deployment/tx/uni-7-dry-run +python3 scripts/check_juno_v1_dry_run_txs.py +``` + +For one-off inspection, use the extractor directly: + +```sh +python3 scripts/extract_juno_v1_tx_sets.py \ + --code-id astroport-factory=store-factory.json \ + --address astroport-factory=instantiate-factory.json +``` + +Use `--scan tx.json` first when a tx response shape is unfamiliar; it prints discovered `code_id` and contract address values without mapping them. + +## Render command shape + +Set shell variables from real uni-7 outputs, then render and validate: + +```sh +python3 scripts/fill_juno_v1_deployment_config.py \ + --output deployment/juno-v1-testnet.json \ + --require-complete \ + --set accounts.owner="$JUNO_OWNER" \ + --set accounts.guardian="$JUNO_GUARDIAN" \ + --set accounts.treasury="$JUNO_TREASURY" \ + --set accounts.tokenfactory_module="$JUNO_TOKENFACTORY_MODULE" \ + --set code_ids.astroport-factory="$CODE_ID_FACTORY" \ + --set code_ids.astroport-incentives="$CODE_ID_INCENTIVES" \ + --set code_ids.astroport-native-coin-registry="$CODE_ID_NATIVE_COIN_REGISTRY" \ + --set code_ids.astroport-oracle="$CODE_ID_ORACLE" \ + --set code_ids.astroport-pair="$CODE_ID_PAIR" \ + --set code_ids.astroport-router="$CODE_ID_ROUTER" \ + --set code_ids.astroport-tokenfactory-tracker="$CODE_ID_TOKENFACTORY_TRACKER" \ + --set code_ids.astroport-whitelist="$CODE_ID_WHITELIST" \ + --set code_ids.cw20-base="$CODE_ID_CW20_BASE" \ + --set addresses.astroport-factory="$ADDR_FACTORY" \ + --set addresses.astroport-incentives="$ADDR_INCENTIVES" \ + --set addresses.astroport-native-coin-registry="$ADDR_NATIVE_COIN_REGISTRY" \ + --set addresses.astroport-oracle="$ADDR_ORACLE" \ + --set addresses.astroport-router="$ADDR_ROUTER" \ + --set addresses.astroport-tokenfactory-tracker="$ADDR_TOKENFACTORY_TRACKER" \ + --set addresses.astroport-whitelist="$ADDR_WHITELIST" \ + --set pair_create_msg_template.asset_infos.1.native_token.denom="$FIRST_COUNTERPARTY_DENOM" + +python3 scripts/check_juno_v1_deployment_template.py deployment/juno-v1-testnet.json +``` + +The fill script rewires dependent instantiate fields from the top-level values, including factory pair code ID, whitelist/tracker config, router factory address, native incentives denom, and oracle asset info. + +## Frontend consumption + +After rendering `deployment/juno-v1-testnet.json`, copy or import it alongside the generated declaration file: + +```ts +import deployment from "./juno-v1-testnet.json"; +import type { JunoV1FrontendDeploymentConfig } from "./juno-v1-frontend-config"; + +const config = deployment satisfies JunoV1FrontendDeploymentConfig; +``` + +Use `config.addresses` for the canonical contract map. For launch UX, the first pool form can start from `config.pair_create_msg_template`, but existing pools must be discovered by querying the factory; do not bake pool addresses into frontend config. + +Required frontend addresses: `astroport-factory`, `astroport-router`, `astroport-native-coin-registry`, `astroport-incentives`. +Optional frontend addresses: `astroport-oracle`. + +See `juno-v1-frontend-config.example.ts` for a dependency-free fixture that demonstrates the exact address map and first XYK pair-create helper. + +## Scope guardrails + +- v1 is XYK-only and permissionless. +- No new DEX token is introduced; incentives use the configured native denom. +- Do not add stable pairs, LSTs, perps, or yield surfaces to this config. +- Frontend should read canonical contract addresses from `addresses` and discover pools through factory queries. diff --git a/deployment/frontend-release-checklist.md b/deployment/frontend-release-checklist.md new file mode 100644 index 000000000..e17d051dc --- /dev/null +++ b/deployment/frontend-release-checklist.md @@ -0,0 +1,41 @@ +# Astroport-Juno v1 frontend release checklist + +Use this when real uni-7 values exist and the UI repo is ready to consume the DEX handoff. This is deliberately narrow: publish the rendered contract map and generated type, then verify the UI discovers pools through the factory. + +## Release files to hand to the UI repo + +Copy these files together from `deployment/`: + +- `juno-v1-testnet.json` — rendered uni-7 config produced from real upload/instantiate tx output; keep local/private until stewards choose to publish. +- `juno-v1-frontend-config.d.ts` — generated TypeScript contract for the frontend handoff. +- `juno-v1-frontend-config.example.ts` — optional fixture showing the address map and first XYK pair create helper. + +Do not copy `juno-v1-testnet.template.json` as the live config. + +## Pre-copy verification + +Run from repo root before handing files to the UI repo: + +```sh +python3 scripts/check_juno_v1_deployment_template.py deployment/juno-v1-testnet.json +python3 scripts/check_juno_v1_frontend_config.py deployment/juno-v1-testnet.json +python3 scripts/generate_juno_v1_frontend_types.py --check +python3 scripts/check_juno_v1_frontend_example.py +python3 scripts/check_juno_v1_frontend_handoff_sync.py +``` + +The first two commands require the rendered `deployment/juno-v1-testnet.json`; rehearse without chain output via `python3 scripts/check_juno_v1_dry_run_txs.py`. + +## Frontend address surface + +Required frontend addresses: `astroport-factory`, `astroport-router`, `astroport-native-coin-registry`, `astroport-incentives`. +Optional frontend addresses: `astroport-oracle`. + +Use `config.addresses` as the canonical contract map. Use `config.pair_create_msg_template` only to seed the first XYK create-pair form; discover existing pools by querying the factory contract. Do not hardcode pools or pair addresses in the UI repo. + +## Scope guardrails + +- v1 is XYK-only and permissionless. +- No new DEX token is introduced; incentives use the configured native denom. +- Do not add stable pairs, PCL, LSTs, perps, or yield surfaces to this handoff. +- Release blockers are missing real code IDs/addresses, stale generated types, or UI code that bypasses factory pair discovery. diff --git a/deployment/indexer-performance-benchmark-runbook.md b/deployment/indexer-performance-benchmark-runbook.md new file mode 100644 index 000000000..21fccf22b --- /dev/null +++ b/deployment/indexer-performance-benchmark-runbook.md @@ -0,0 +1,247 @@ +# Indexer staging performance benchmark runbook + +This runbook measures catch-up throughput for the active TypeScript indexer in `services/indexer/` against a staging Postgres database and non-public archive RPC/LCD endpoints. It is intentionally manual: it does not provision infrastructure, cut production traffic over, or publish fabricated acceptance metrics. + +## Scope and acceptance evidence + +Collect one JSON summary for each benchmark range and attach the raw output to the release/issue. Each summary must include: + +- block range (`blockRange.from`, `blockRange.to`) +- duration (`durationMs`, `durationSeconds`) +- blocks/sec (`blocksPerSecond`) +- cursor, head, target, and lag (`cursor`, `head`, `target`, `lag`) +- fatal RPC/LCD error count observed by the harness (`rpcErrorCount`) +- indexed event counts where available (`eventCounts.*`); use `null` rather than inventing unavailable values + +The lightweight harness added in this branch prints exactly one machine-readable JSON object per run. Non-fatal reserve snapshot LCD failures are currently logged by the indexer as `indexer_reserve_snapshot_failed`; review stderr/log output alongside the JSON summary until snapshot workers expose first-class counters. + +## Required staging inputs + +Use an isolated staging environment. Do not run this against production tables or public/free RPC endpoints. + +Required environment: + +```bash +export DATABASE_URL='postgres://:@:5432/' +export JUNO_RPC_URL='https://' +export JUNO_REST_URL='https://' +export CHAIN_ID='juno-1' +export CURSOR_ID='astroport-juno-v1-benchmark' +export INDEXER_MODE='catchup' + +# Runtime knobs from IDX-PERF-01. Use the same names in staging and CI. +export RANGE_SIZE='10000' +export FETCH_WINDOW_SIZE='250' +export FETCH_CONCURRENCY='32' +export REALTIME_FETCH_CONCURRENCY='8' +export RPC_TIMEOUT_MS='10000' +export RPC_MAX_RETRIES='5' +export INGEST_RESERVE_SNAPSHOTS_INLINE='false' +export INGEST_CANDLES_INLINE='false' +export INGEST_AGGREGATES_INLINE='false' +export PRICE_DEV_MOCKS='false' +export CONFIRMATION_DEPTH='2' +export BATCH_SIZE='50' +``` + +Current-branch notes: + +- `INDEXER_MODE`, `INGEST_RESERVE_SNAPSHOTS_INLINE`, and `INGEST_CANDLES_INLINE` are the intended staging contract from IDX-PERF-01. If this benchmark branch is run before IDX-PERF-01 lands, those flags may be documented but not yet consumed by `loadConfig()`. +- Inline reserve snapshots are still attempted after swap/provide/withdraw events until the deployed build wires `INGEST_RESERVE_SNAPSHOTS_INLINE` through the `Indexer`; use event-light ranges for low-event throughput and record LCD behavior honestly. +- Candle writes are currently performed inline when complete asset decimals are present until the deployed build wires `INGEST_CANDLES_INLINE` through the DB writer. If disabling inline candles is not available in the deployed build, note that in the benchmark evidence. +- Use a dedicated `CURSOR_ID` per run or range family so benchmark cursor rewinds do not affect other staging workers. + +## Install and migrate + +```bash +cd services/indexer +npm ci +npm run migrate +``` + +## Select ranges + +Pick and record exact heights before running. Use known historical chain data, provider dashboards, or prior staging observations. + +1. **Low-event range:** 10,000 consecutive finalized blocks with few/no Astroport events. +2. **Event-heavy range:** a known range containing swaps, provide/withdraw liquidity, pool creation, or incentive events. +3. **Realtime catch-up:** after historical benchmarks, restart normal catch-up and measure lag trending back toward zero. + +Example placeholders below must be replaced with real heights: + +```bash +export LOW_FROM= +export LOW_TO=$((LOW_FROM + 9999)) +export HEAVY_FROM= +export HEAVY_TO= +``` + +## Benchmark commands + +The harness rewinds the configured benchmark cursor to `from-height - 1`, runs until `to-height`, then prints one JSON summary. Keep stdout/stderr logs with the issue evidence. + +### 1. 10,000-block low-event range + +```bash +cd services/indexer +npm --silent run benchmark:range -- --from-height="$LOW_FROM" --to-height="$LOW_TO" | tee benchmark-low-event.json +``` + +### 2. Known event-heavy range + +```bash +cd services/indexer +npm --silent run benchmark:range -- --from-height="$HEAVY_FROM" --to-height="$HEAVY_TO" | tee benchmark-event-heavy.json +``` + +### 3. Realtime catch-up after benchmark + +Use a fresh cursor or explicitly set the benchmark cursor near the current staging cursor before starting the normal indexer. Then watch lag until it stabilizes near the configured confirmation depth. + +```bash +cd services/indexer +npm run dev 2>&1 | tee benchmark-realtime-catchup.log +``` + +In another shell, sample cursor/head/target and backlog with the SQL snippets below. Record at least start, 5-minute, and 15-minute samples, or until lag is stable. + +## SQL snippets + +Run with `psql "$DATABASE_URL"`. Set variables first: + +```sql +\set chain_id 'juno-1' +\set cursor_id 'astroport-juno-v1-benchmark' +\set from_height 39381297 +\set to_height 39391296 +``` + +### Cursor height + +```sql +SELECT id, chain_id, last_height, last_block_hash, updated_at +FROM indexer_cursors +WHERE id = :'cursor_id'; +``` + +### Processed block count for a range + +```sql +SELECT count(*) AS processed_blocks, + min(height) AS min_height, + max(height) AS max_height +FROM processed_blocks +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height; +``` + +### Swaps and liquidity counts for a range + +```sql +SELECT 'swaps' AS table_name, count(*) AS rows +FROM swaps +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height +UNION ALL +SELECT 'liquidity_events' AS table_name, count(*) AS rows +FROM liquidity_events +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height +UNION ALL +SELECT 'liquidity_provides' AS table_name, count(*) AS rows +FROM liquidity_events +WHERE chain_id = :'chain_id' + AND kind = 'provide' + AND height BETWEEN :from_height AND :to_height +UNION ALL +SELECT 'liquidity_withdraws' AS table_name, count(*) AS rows +FROM liquidity_events +WHERE chain_id = :'chain_id' + AND kind = 'withdraw' + AND height BETWEEN :from_height AND :to_height; +``` + +### Job backlog depth / lag proxy + +There is no separate job queue table in the current indexer schema. Use cursor-to-target lag as the backlog proxy: + +```sql +WITH cursor_row AS ( + SELECT last_height + FROM indexer_cursors + WHERE id = :'cursor_id' +), target AS ( + SELECT max(height) AS latest_processed_height + FROM processed_blocks + WHERE chain_id = :'chain_id' +) +SELECT cursor_row.last_height AS cursor_height, + target.latest_processed_height, + GREATEST(target.latest_processed_height - cursor_row.last_height, 0) AS processed_block_backlog_proxy +FROM cursor_row, target; +``` + +For realtime catch-up, compare `last_height` from the cursor query to the node head reported in harness output or `/status`, then subtract `CONFIRMATION_DEPTH` to compute target lag. + +### Staging cleanup + +Only run cleanup on isolated staging data and only for the benchmark cursor/range. Take a database snapshot first if the data may be needed for debugging. + +```sql +BEGIN; + +DELETE FROM token_candles +WHERE chain_id = :'chain_id' + AND bucket_start IN ( + SELECT DISTINCT date_trunc('minute', block_time) + FROM processed_blocks + WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height + ); + +DELETE FROM pool_state_snapshots +WHERE height BETWEEN :from_height AND :to_height; + +DELETE FROM incentive_events +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height; + +DELETE FROM liquidity_events +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height; + +DELETE FROM swaps +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height; + +DELETE FROM pools +WHERE chain_id = :'chain_id' + AND created_height BETWEEN :from_height AND :to_height; + +DELETE FROM processed_blocks +WHERE chain_id = :'chain_id' + AND height BETWEEN :from_height AND :to_height; + +DELETE FROM indexer_cursors +WHERE id = :'cursor_id'; + +COMMIT; +``` + +If staging contains pre-existing rows in the same range, prefer restoring a staging snapshot over manual deletes. + +## Interpreting results + +Provider throttling symptoms: + +- `rpcErrorCount` is greater than zero, or logs contain HTTP 429/5xx, fetch timeouts, `/block`, `/block_results`, `/status`, or `indexer_reserve_snapshot_failed` LCD smart-query failures. +- Throughput falls while database CPU, locks, and connection utilization remain low. +- Increasing `BATCH_SIZE` does not improve blocks/sec, or makes errors worse. + +Database saturation symptoms: + +- Fatal RPC/LCD errors remain zero and logs do not show repeated non-fatal LCD failures, but blocks/sec drops as Postgres CPU, I/O wait, lock waits, or connection pool wait time rises. +- Heavy ranges are disproportionately slower than low-event ranges because inserts/upserts dominate. +- Swaps/liquidity/candle/snapshot writes increase sharply during the slow window. + +When results are ambiguous, rerun the low-event range with a lower `BATCH_SIZE` and compare with the heavy range. Do not average away throttling spikes; report p50/p95 samples or the raw run JSON/logs if collected. diff --git a/deployment/indexer-staging-backfill-runbook.md b/deployment/indexer-staging-backfill-runbook.md new file mode 100644 index 000000000..1a4c09852 --- /dev/null +++ b/deployment/indexer-staging-backfill-runbook.md @@ -0,0 +1,271 @@ +# Juno DEX indexer staging deployment runbook + +Use this to try the indexer/API on staging before any public frontend production traffic depends on it. + +## Decision + +- **Goal:** prove the merged indexer can run against real Juno data, serve honest API responses, and support a frontend preview. +- **Scope:** staging database, staging API URL, bounded backfill, smoke checks, frontend preview env. +- **Non-goal:** production cutover. Do not point the public production frontend at the indexer until the pass criteria below are met. + +## Preconditions + +- `main` is at or after `bef1d519 fix: ignore unknown pair events (#108)`. +- GitHub CI is green for the commit being deployed. +- Managed or disposable staging Postgres is available. +- The RPC/LCD provider supports archive access from `START_HEIGHT=39381297` and height-pinned LCD smart queries. +- Staging has a stable HTTPS API URL, e.g. `https://juno-dex-indexer-staging.`. +- Secrets are set in the host secret manager; do not commit real credentials. + +## Required environment + +Set these for the indexer service/container: + +```bash +DATABASE_URL='postgres://:@:5432/?sslmode=require' +JUNO_RPC_URL='https://' +JUNO_REST_URL='https://' +JUNO_WS_URL='wss:///websocket' +CHAIN_ID='juno-1' +FACTORY_ADDRESS='juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca' +ROUTER_ADDRESS='juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s' +INCENTIVES_ADDRESS='juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598' +ORACLE_ADDRESS='juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p' +NATIVE_COIN_REGISTRY_ADDRESS='juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2' +START_HEIGHT='39381297' +CONFIRMATION_DEPTH='2' +POLL_INTERVAL_MS='5000' +BATCH_SIZE='20' +DRY_RUN='false' +API_PORT='8787' +PRICE_DEV_MOCKS='false' +``` + +Notes: + +- `DATABASE_URL` should require TLS when the provider supports it. +- `START_HEIGHT=39381297` is the recorded Juno v1 factory deployment height. Do **not** use `1` for staging. +- USD pricing remains incomplete; USD TVL/volume/fees may be `null`. That is expected and preferable to fake zeroes. + +## Local preflight from the deploy commit + +Run before building/pushing the staging image: + +```bash +git checkout main +git pull --ff-only origin main + +cd services/indexer +npm ci +npm test +npm run typecheck +npm run build + +# Proves built dist resolves ./migrations from runtime CWD. +node --input-type=module -e 'import("./dist/src/db.js").then(async (m) => console.log((await m.listMigrationFiles()).join(",")))' + +cd ../../frontend +npm ci +npm run typecheck +``` + +Expected migration listing: + +```text +001_init.sql,002_pool_candles.sql,003_api_pricing_readiness.sql,004_pool_state_source_precedence.sql +``` + +## Deploy staging indexer/API + +Build from `services/indexer` using its Dockerfile. The container default command is: + +```bash +node dist/src/migrate.js && node dist/src/index.js +``` + +Recommended host settings: + +| Setting | Value | +|---|---| +| Build context | `services/indexer` | +| Dockerfile | `services/indexer/Dockerfile` | +| Exposed port | `8787` | +| Health path | `/health` | +| Readiness path | `/ready` | +| Metrics path | `/metrics` | +| Replicas | `1` for first staging run | + +After deploy, check logs for successful migrations and startup: + +```bash +# Host-specific command; examples: +# fly logs -a +# railway logs --service +# docker logs +``` + +The service should start without migration path errors and listen on `API_PORT`. + +## First bounded backfill + +Run the bounded backfill as a one-off job with the same image/env. This proves the historical path before allowing the long-lived poller to catch up. + +```bash +cd services/indexer +START_HEIGHT=39381297 CONFIRMATION_DEPTH=2 BATCH_SIZE=20 \ + npm run backfill:range -- --to-height=39381355 +``` + +If running inside the built container image instead of source checkout, use: + +```bash +node dist/src/migrate.js +START_HEIGHT=39381297 CONFIRMATION_DEPTH=2 BATCH_SIZE=20 \ + node dist/src/backfill-range.js --to-height=39381355 +``` + +Verify the cursor reached the smoke-test height: + +```bash +psql "$DATABASE_URL" -c \ + "select id, last_height, last_block_hash, updated_at from indexer_cursors where id = 'astroport-juno-v1' and last_height >= 39381355;" +``` + +Expected: one row for `astroport-juno-v1` with `last_height >= 39381355`. + +## Optional candle repair/backfill + +Run only after swaps exist. Use a narrow known pair/time window first: + +```bash +npm run backfill:candles -- \ + --pair= \ + --from=2026-07-01T00:00:00Z \ + --to=2026-07-02T00:00:00Z \ + --limit=10000 +``` + +Expected: candle rows may be skipped if decimal metadata is incomplete. Do not treat missing candles as success until `asset_metadata` coverage is verified. + +## API smoke checks + +Set: + +```bash +INDEXER_URL='https://juno-dex-indexer-staging.' +``` + +Then run: + +```bash +curl -fsS "$INDEXER_URL/health" | jq . +curl -fsS "$INDEXER_URL/ready" | jq . +curl -fsS "$INDEXER_URL/openapi.json" | jq '.paths | keys' +curl -fsS "$INDEXER_URL/metrics" | grep '^juno_indexer_' +curl -fsS "$INDEXER_URL/stats" | jq . +curl -fsS "$INDEXER_URL/prices" | jq . +curl -fsS "$INDEXER_URL/pools?limit=10" | jq . +``` + +For a pair returned by `/pools`, smoke detail/candles: + +```bash +PAIR='' +curl -fsS "$INDEXER_URL/pools/$PAIR" | jq . +curl -fsS "$INDEXER_URL/pools/$PAIR/candles?interval=1h&limit=10" | jq . +``` + +Wallet history smoke, using the Juno agent wallet as a low-risk test address: + +```bash +curl -fsS "$INDEXER_URL/wallets/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/history?limit=10" | jq . +``` + +## Database smoke checks + +Run against staging Postgres: + +```sql +select version, applied_at from schema_migrations order by version; +select id, chain_id, last_height, last_block_hash, updated_at from indexer_cursors; +select height, block_hash, parent_hash, tx_count, processed_at from processed_blocks order by height desc limit 10; +select pair_address, created_height, created_tx_hash from pools order by created_height limit 10; +select pair_address, pool_id, height, offer_asset, ask_asset, offer_amount, return_amount from swaps order by height limit 10; +select pair_address, pool_id, kind, height, assets, share_amount from liquidity_events order by height limit 10; +select p.pair_address, s.height, s.source, s.reserves, s.total_share from pool_state_snapshots s join pools p on p.id = s.pool_id order by s.height desc limit 10; +select pair_address, pool_id, interval, bucket_start, volume, volume_quote, volume_usd from token_candles order by bucket_start desc limit 10; +``` + +Expected: + +- `schema_migrations` includes all four migrations. +- Cursor advances and `updated_at` changes while the poller runs. +- `processed_blocks` rows have real block hashes and parent hashes. +- Pools have real pair addresses from factory events. +- Swap/liquidity rows, if present, have non-null `pool_id`. +- Unknown pair-like events are not persisted as swaps/liquidity. +- `pool_state_snapshots.source='lcd'` rows appear for touched known pairs when LCD supports the requested height. +- `volume_usd` remains `null` unless a USD pricing worker explicitly populates it. + +## Frontend staging/preview + +Set frontend preview env: + +```bash +VITE_DEX_INDEXER_URL='https://juno-dex-indexer-staging.' +VITE_DEX_INDEXER_DISABLED='false' +``` + +Keep existing Juno RPC/REST/frontend env values unchanged. + +Deploy a frontend preview and smoke: + +1. Open the preview URL. +2. Confirm the app loads without console errors. +3. Confirm analytics panels do **not** show fake zeroes when indexer stats are unavailable/null. +4. Confirm pool list renders with indexer-backed metadata/reserves where present. +5. Open a pool detail page from a `/pools` result. +6. Connect or paste a wallet and check wallet history/positions degrade gracefully if empty. +7. Temporarily set `VITE_DEX_INDEXER_DISABLED=true` in a separate preview only if you need to compare fallback behavior. + +Browser/API checks: + +```bash +curl -I 'https:///' +curl -fsS "$VITE_DEX_INDEXER_URL/health" +curl -fsS "$VITE_DEX_INDEXER_URL/ready" +``` + +## Pass criteria + +Staging is considered ready for a limited production-candidate trial when all are true: + +- `/ready` returns HTTP 200 with `status: "ready"`. +- `/health` reports non-null `cursorHeight`, `headHeight`, `confirmedTargetHeight`, and lag fields. +- Cursor continues advancing after the bounded backfill. +- `/metrics` exposes `juno_indexer_*` gauges and scrape does not error. +- `/pools` returns real pools with `dataSource: "indexer"` and `isMock: false`. +- At least one pool detail response includes reserves from persisted snapshots, or missing reserves are explainable by LCD/archive limitations. +- Swap/liquidity rows, if present, reference known pools via `pool_id`. +- USD TVL/volume/fee fields are `null` when not priced, not fabricated as `0`. +- Frontend preview works with the staging indexer URL and falls back gracefully for missing data. +- Logs do not show repeated migration failures, reorg/hash conflicts, LCD timeout storms, or RPC rate-limit loops. + +## Stop / rollback triggers + +Stop staging and investigate before continuing if any occur: + +- Container fails before API startup due migration directory/path errors. +- `/ready` remains `not_ready` after migrations, DB, and RPC are expected valid. +- Cursor does not advance after several polling intervals. +- Bounded backfill exits before `last_height >= --to-height`. +- `processed_blocks` reports parent/hash conflicts. +- Event rows have synthetic-looking tx hashes for real transactions. +- Swap/liquidity rows have `pool_id IS NULL`. +- API returns mock data or fabricated zero USD aggregates. +- Frontend preview crashes or silently presents unavailable analytics as real data. +- RPC/LCD provider rate limits dominate logs. + +## Production cutover rule + +Only after staging passes: set production frontend `VITE_DEX_INDEXER_URL` to the stable production indexer URL, preferably behind a fallback/circuit-breaker rollout. Until then, production frontend should continue to treat the indexer as optional. \ No newline at end of file diff --git a/deployment/juno-v1-frontend-config.d.ts b/deployment/juno-v1-frontend-config.d.ts new file mode 100644 index 000000000..4b865840e --- /dev/null +++ b/deployment/juno-v1-frontend-config.d.ts @@ -0,0 +1,44 @@ +// AUTO-GENERATED by scripts/generate_juno_v1_frontend_types.py; do not edit by hand. +// Source: deployment/juno-v1-testnet.template.json + +export type JunoV1CodeIdKey = "astroport-factory" | "astroport-incentives" | "astroport-native-coin-registry" | "astroport-oracle" | "astroport-pair" | "astroport-router" | "astroport-tokenfactory-tracker" | "astroport-whitelist" | "cw20-base"; + +export type JunoV1AddressKey = "astroport-factory" | "astroport-incentives" | "astroport-native-coin-registry" | "astroport-oracle" | "astroport-router" | "astroport-tokenfactory-tracker" | "astroport-whitelist"; + +export type JunoV1RequiredFrontendAddressKey = "astroport-factory" | "astroport-router" | "astroport-native-coin-registry" | "astroport-incentives"; + +export type JunoV1OptionalFrontendAddressKey = "astroport-oracle"; + +export type JunoV1FrontendAddressKey = + | JunoV1RequiredFrontendAddressKey + | JunoV1OptionalFrontendAddressKey; + +export type NativeAssetInfo = { native_token: { denom: string } }; +export type XykPairType = { xyk: Record }; + +export interface JunoV1DeploymentNetwork { + chain_id: "uni-7" | "juno-1" | string; + bech32_prefix: "juno"; + fee_denom: string; + native_asset_denom: "ujunox" | string; +} + +export interface JunoV1FrontendConfig { + required_addresses: JunoV1RequiredFrontendAddressKey[]; + optional_addresses: JunoV1OptionalFrontendAddressKey[]; + pair_discovery: string; +} + +export interface JunoV1PairCreateMsgTemplate { + pair_type: XykPairType; + asset_infos: [NativeAssetInfo, NativeAssetInfo]; + init_params: null; +} + +export interface JunoV1FrontendDeploymentConfig { + network: JunoV1DeploymentNetwork; + code_ids: Record; + addresses: Record; + pair_create_msg_template: JunoV1PairCreateMsgTemplate; + frontend: JunoV1FrontendConfig; +} diff --git a/deployment/juno-v1-frontend-config.example.ts b/deployment/juno-v1-frontend-config.example.ts new file mode 100644 index 000000000..425045c37 --- /dev/null +++ b/deployment/juno-v1-frontend-config.example.ts @@ -0,0 +1,73 @@ +// Example consumer fixture for the Astroport-Juno v1 frontend handoff. +// It intentionally mirrors deployment/juno-v1-testnet.template.json placeholders; +// replace values by importing a rendered deployment/juno-v1-testnet.json at launch. + +import type { + JunoV1FrontendAddressKey, + JunoV1FrontendDeploymentConfig, +} from "./juno-v1-frontend-config"; + +export const junoV1FrontendConfigExample = { + network: { + chain_id: "uni-7", + bech32_prefix: "juno", + fee_denom: "ujunox", + native_asset_denom: "ujunox", + }, + code_ids: { + "astroport-factory": 0, + "astroport-incentives": 0, + "astroport-native-coin-registry": 0, + "astroport-oracle": 0, + "astroport-pair": 0, + "astroport-router": 0, + "astroport-tokenfactory-tracker": 0, + "astroport-whitelist": 0, + "cw20-base": 0, + }, + addresses: { + "astroport-factory": "juno1replacefactory000000000000000000000000000000", + "astroport-incentives": "juno1replaceincentives00000000000000000000000000", + "astroport-native-coin-registry": "juno1replacecoinregistry00000000000000000000000", + "astroport-oracle": "juno1replaceoracle0000000000000000000000000000000", + "astroport-router": "juno1replacerouter000000000000000000000000000000", + "astroport-tokenfactory-tracker": "juno1replacetracker0000000000000000000000000000", + "astroport-whitelist": "juno1replacewhitelist00000000000000000000000000", + }, + pair_create_msg_template: { + pair_type: { xyk: {} }, + asset_infos: [ + { native_token: { denom: "ujunox" } }, + { native_token: { denom: "ibc/REPLACE_COUNTERPARTY_DENOM_HASH" } }, + ], + init_params: null, + }, + frontend: { + required_addresses: [ + "astroport-factory", + "astroport-router", + "astroport-native-coin-registry", + "astroport-incentives", + ], + optional_addresses: ["astroport-oracle"], + pair_discovery: "query astroport-factory pairs/pair; do not hardcode pools before launch", + }, +} satisfies JunoV1FrontendDeploymentConfig; + +export function frontendAddressMap( + config: JunoV1FrontendDeploymentConfig, +): Record { + return { + "astroport-factory": config.addresses["astroport-factory"], + "astroport-router": config.addresses["astroport-router"], + "astroport-native-coin-registry": config.addresses["astroport-native-coin-registry"], + "astroport-incentives": config.addresses["astroport-incentives"], + "astroport-oracle": config.addresses["astroport-oracle"], + }; +} + +export function firstXykPairCreateMsg(config: JunoV1FrontendDeploymentConfig) { + // Frontends should use this as a create-pair message template only. + // Existing pools/pairs must be discovered from the factory contract, not hardcoded here. + return config.pair_create_msg_template; +} diff --git a/deployment/juno-v1-readiness-plan.md b/deployment/juno-v1-readiness-plan.md new file mode 100644 index 000000000..007e9071e --- /dev/null +++ b/deployment/juno-v1-readiness-plan.md @@ -0,0 +1,327 @@ +# Astroport-Juno v1 deployment/readiness plan + +Date: 2026-06-29T17:22:23Z +Scope: Juno DEX v1 contracts/config only. This is an operator checklist and blocker list; it is not authorization to broadcast transactions. + +## 0. Current verified state + +- Repo branch: `juno-agent/dex-guards-20260629` at `c1623b5b chore(juno): remove stale non-v1 schemas`. +- `junod`: `/opt/data/bin/junod`, version `v29.0.0`. +- Mainnet RPC check: `https://juno-rpc.publicnode.com:443/status` returned `network=juno-1`, `catching_up=false`. +- Mainnet tokenfactory module account query returned `juno19ejy8n9qsectrf4semdp9cpknflld0j6tj7k2a`. +- Mainnet wasm params query returned `code_upload_access.permission=Everybody` and `instantiate_default_permission=Everybody`; re-query immediately before any real upload/broadcast because chain params can change. +- `cosmwasm-check`: `/usr/local/bin/cosmwasm-check`, version `3.0.9`. +- Docker is installed but daemon is not reachable here, so `scripts/build_release.sh` cannot produce optimized artifacts in this environment. +- No real optimized v1 artifact directory exists yet; only a test fixture wasm was found under `contracts/periphery/tokenfactory_tracker/tests/test_data/`. + +## 1. Required v1 wasm artifacts + +Final optimized artifact set must contain exactly these eight files and no deferred/stable/PCL/tokenomics extras: + +1. `artifacts/astroport_factory.wasm` +2. `artifacts/astroport_pair.wasm` +3. `artifacts/astroport_router.wasm` +4. `artifacts/astroport_native_coin_registry.wasm` +5. `artifacts/astroport_oracle.wasm` +6. `artifacts/astroport_tokenfactory_tracker.wasm` +7. `artifacts/astroport_whitelist.wasm` +8. `artifacts/astroport_incentives.wasm` + +Build/check commands, run in an environment with Docker daemon access: + +```sh +cd /opt/data/repos/astroport-core +scripts/build_release.sh +python3 scripts/check_juno_v1_artifacts.py artifacts +for wasm in \ + artifacts/astroport_factory.wasm \ + artifacts/astroport_pair.wasm \ + artifacts/astroport_router.wasm \ + artifacts/astroport_native_coin_registry.wasm \ + artifacts/astroport_oracle.wasm \ + artifacts/astroport_tokenfactory_tracker.wasm \ + artifacts/astroport_whitelist.wasm \ + artifacts/astroport_incentives.wasm; do + cosmwasm-check --available-capabilities staking,cosmwasm_1_1,cosmwasm_2_0,iterator,stargate "$wasm" +done +``` + +`cw20-base` is also required as a code ID in the deployment config because the factory instantiate schema still carries `token_code_id`, even though Juno v1 LP shares are TokenFactory-native. Use a known verified cw20-base code ID for the target network or upload a pinned cw20-base artifact separately. + +## 2. Required code IDs and addresses + +`deployment/juno-v1-testnet.template.json` expects these code IDs: + +- `code_ids.astroport-factory` +- `code_ids.astroport-incentives` +- `code_ids.astroport-native-coin-registry` +- `code_ids.astroport-oracle` +- `code_ids.astroport-pair` +- `code_ids.astroport-router` +- `code_ids.astroport-tokenfactory-tracker` +- `code_ids.astroport-whitelist` +- `code_ids.cw20-base` + +Final frontend/deployment handoff expects these instantiated addresses: + +- `addresses.astroport-native-coin-registry` +- `addresses.astroport-whitelist` +- `addresses.astroport-factory` +- `addresses.astroport-incentives` +- `addresses.astroport-router` +- `addresses.astroport-oracle` +- `addresses.astroport-tokenfactory-tracker` + +Launch-critical addresses for frontend: factory, native coin registry, router, incentives. Oracle and standalone tokenfactory tracker can be deployed/dormant, but the factory pair tracker config must be correct before public pool creation. + +## 3. Instantiate/update order + +Use this order to avoid circular dependencies. The rendered config represents the final state, but the first factory instantiate cannot normally know the incentives contract address unless using address precomputation/Instantiate2. + +1. Store all v1 wasms and capture code IDs. +2. Resolve manual accounts: + - `JUNO_OWNER`: DAO/steward owner/admin. + - `JUNO_GUARDIAN`: incentives guardian. + - `JUNO_TREASURY`: fee destination and v1 incentives vesting placeholder. + - `JUNO_TOKENFACTORY_MODULE`: tokenfactory module account (`juno19ejy8n9qsectrf4semdp9cpknflld0j6tj7k2a` on current juno-1 query; re-query before mainnet use). +3. Instantiate `astroport-native-coin-registry` with owner. +4. Execute native coin registry `add`/`register` for `ujuno` plus verified launch IBC denoms and decimals. +5. Instantiate `astroport-whitelist` with owner admin and `mutable=true`. +6. Instantiate `astroport-factory` with: + - `coin_registry_address` = native coin registry address, + - `owner` = DAO/steward owner, + - `fee_address` = treasury, + - `generator_address` = `null` for the initial instantiate unless the incentives address is safely precomputed, + - one XYK pair config only: pair code ID, `total_fee_bps=30`, `maker_fee_bps=0`, `permissioned=true`, not disabled. Keep it permissioned until the official first pair is created and seeded. + - `token_code_id` = cw20-base code ID, + - `whitelist_code_id` = whitelist code ID, + - `tracker_config.code_id` = tokenfactory tracker code ID, + - `tracker_config.token_factory_addr` = tokenfactory module account. +7. Instantiate `astroport-incentives` with native denom (`reward_token={"native_token":{"denom":"ujuno"}}` on mainnet, `ujunox` on uni-7), factory address, owner, and guardian. Do not use legacy `astro_token` or `vesting_contract` fields. +8. Execute factory `update_config` to set `generator_address` to the incentives address. +9. Instantiate `astroport-router` with factory address. +10. Instantiate `astroport-oracle` only for a real pair asset vector when a pool exists; otherwise leave it out of launch-critical UI and do not pretend it proves readiness. +11. Instantiate standalone `astroport-tokenfactory-tracker` only if an operator-facing tracker address is still desired; factory-created pair trackers are the launch-critical path. +12. Verify the official first pair does not already exist by querying factory `pair`/`pairs` for the launch asset infos; stop if any unexpected pair exists. +13. Create the official first XYK pool through factory `create_pair`; wait for pair address and LP denom. +14. Immediately provide official seed liquidity. +15. Query factory pair registry and pool balances; require the official pair address and non-zero liquidity. +16. After smoke checks pass, execute factory `update_pair_config` for XYK with the same fees/code ID and `permissioned=false` to open public pair creation. +17. Query factory/pair/router/config state and produce `deployment/juno-v1-testnet.json` or mainnet equivalent from tx output. + +If the team chooses Instantiate2/address precomputation, document the salt, code checksum, creator, predicted addresses, and verification query before deviating from the update-after-incentives path. + +## 4. Pool factory config checks + +Factory `Config {}` must show: + +- owner = approved DAO/steward address. +- one `pair_configs` entry only. +- pair type = `{ "xyk": {} }`. +- pair code ID = uploaded `astroport_pair.wasm` code ID. +- `is_disabled=false` and `is_generator_disabled=false` unless intentionally freezing launch. +- `permissioned=true` during the first-pool gate. It may become `permissioned=false` for public pool creation only after the official first pair is registered, seeded, and smoke-checked. +- `total_fee_bps=30`, `maker_fee_bps=0` unless governance explicitly changes fees. +- `coin_registry_address` = deployed native coin registry. +- `whitelist_code_id` = deployed whitelist code ID. +- `generator_address` = incentives address after step 8, or `null` if incentives are deliberately dormant and UI hides rewards. + +Native coin registry must return correct decimals for each launch denom. Do not launch a pool in the UI until denom trace + decimals + explorer links are verified. + +## 5. No-broadcast dry-run commands + +Set these once per target. Use uni-7 for rehearsal, juno-1 for mainnet generate-only/dry-run checks. + +```sh +# uni-7 rehearsal +export CHAIN_ID=uni-7 +export DENOM=ujunox +export RPC=https://juno-testnet-rpc.polkachu.com +export KEY_NAME=juno-agent +export KEYRING_DIR=/opt/data/.juno-agent +export KEYRING_BACKEND=test +export GAS_PRICES=0.075ujunox + +# mainnet generate-only/dry-run +export CHAIN_ID=juno-1 +export DENOM=ujuno +export RPC=https://juno-rpc.publicnode.com:443 +export KEY_NAME=juno-agent +export KEYRING_DIR=/opt/data/.juno-agent +export KEYRING_BACKEND=test +export GAS_PRICES=0.075ujuno +``` + +Preflight: + +```sh +/opt/data/bin/junod version +curl -sS "$RPC/status" | jq -r '.result.node_info.network, .result.sync_info.catching_up' +/opt/data/bin/junod query auth module-account tokenfactory --node "$RPC" -o json | jq -r '.account.value.address // .account.address' +/opt/data/bin/junod query wasm params --node "$RPC" -o json +/opt/data/bin/junod query feemarket params --node "$RPC" -o json || true +/opt/data/bin/junod keys show "$KEY_NAME" --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" -a +``` + +Store tx generate-only/dry-run pattern, repeat for each artifact: + +```sh +/opt/data/bin/junod tx wasm store artifacts/astroport_factory.wasm \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.5 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --generate-only > /tmp/store-astroport-factory.unsigned.json + +/opt/data/bin/junod tx wasm store artifacts/astroport_factory.wasm \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.5 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --dry-run -o json +``` + +Instantiate generate-only pattern: + +```sh +jq '.instantiate_msgs["astroport-native-coin-registry"]' deployment/juno-v1-testnet.json > /tmp/native-registry-msg.json +/opt/data/bin/junod tx wasm instantiate "$CODE_ID_NATIVE_COIN_REGISTRY" "$(cat /tmp/native-registry-msg.json)" \ + --label 'astroport-juno-v1-native-coin-registry' \ + --admin "$JUNO_OWNER" \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.4 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --generate-only > /tmp/instantiate-native-registry.unsigned.json +``` + +Factory post-incentives update generate-only: + +```sh +/opt/data/bin/junod tx wasm execute "$ADDR_FACTORY" \ + '{"update_config":{"token_code_id":null,"fee_address":null,"generator_address":"'$ADDR_INCENTIVES'","whitelist_code_id":null,"coin_registry_address":null}}' \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.4 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --generate-only > /tmp/update-factory-generator.unsigned.json +``` + +Pair create generate-only: + +```sh +jq '.pair_create_msg_template | {create_pair: .}' deployment/juno-v1-testnet.json > /tmp/create-pair-msg.json +/opt/data/bin/junod tx wasm execute "$ADDR_FACTORY" "$(cat /tmp/create-pair-msg.json)" \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.4 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --generate-only > /tmp/create-pair.unsigned.json +``` + +Open public pair creation only after the first-pool gate passes: + +```sh +jq '.post_update_state["astroport-factory"].pair_configs[0] | {update_pair_config:{config:.}}' deployment/juno-v1-testnet.json > /tmp/open-public-pair-creation-msg.json +/opt/data/bin/junod tx wasm execute "$ADDR_FACTORY" "$(cat /tmp/open-public-pair-creation-msg.json)" \ + --from "$KEY_NAME" --chain-id "$CHAIN_ID" --node "$RPC" \ + --gas auto --gas-adjustment 1.4 --gas-prices "$GAS_PRICES" \ + --keyring-backend "$KEYRING_BACKEND" --keyring-dir "$KEYRING_DIR" \ + --generate-only > /tmp/open-public-pair-creation.unsigned.json +``` + +Do not add `--yes`; do not broadcast from this plan. + +## 6. Readiness verification queries + +After real txs exist, save full `junod -o json` tx responses under `deployment/tx//` and render config: + +```sh +python3 scripts/extract_juno_v1_tx_sets.py \ + --code-id astroport-factory=deployment/tx/uni-7/store-astroport-factory.json \ + --code-id astroport-incentives=deployment/tx/uni-7/store-astroport-incentives.json \ + --code-id astroport-native-coin-registry=deployment/tx/uni-7/store-astroport-native-coin-registry.json \ + --code-id astroport-oracle=deployment/tx/uni-7/store-astroport-oracle.json \ + --code-id astroport-pair=deployment/tx/uni-7/store-astroport-pair.json \ + --code-id astroport-router=deployment/tx/uni-7/store-astroport-router.json \ + --code-id astroport-tokenfactory-tracker=deployment/tx/uni-7/store-astroport-tokenfactory-tracker.json \ + --code-id astroport-whitelist=deployment/tx/uni-7/store-astroport-whitelist.json \ + --code-id cw20-base=deployment/tx/uni-7/store-cw20-base.json \ + --address astroport-factory=deployment/tx/uni-7/instantiate-astroport-factory.json \ + --address astroport-incentives=deployment/tx/uni-7/instantiate-astroport-incentives.json \ + --address astroport-native-coin-registry=deployment/tx/uni-7/instantiate-astroport-native-coin-registry.json \ + --address astroport-oracle=deployment/tx/uni-7/instantiate-astroport-oracle.json \ + --address astroport-router=deployment/tx/uni-7/instantiate-astroport-router.json \ + --address astroport-tokenfactory-tracker=deployment/tx/uni-7/instantiate-astroport-tokenfactory-tracker.json \ + --address astroport-whitelist=deployment/tx/uni-7/instantiate-astroport-whitelist.json \ + > deployment/tx/uni-7/tx-sets.txt + +python3 scripts/build_juno_v1_deployment_command.py \ + --tx-sets deployment/tx/uni-7/tx-sets.txt \ + --owner "$JUNO_OWNER" \ + --guardian "$JUNO_GUARDIAN" \ + --treasury "$JUNO_TREASURY" \ + --tokenfactory-module "$JUNO_TOKENFACTORY_MODULE" \ + --counterparty-denom "$FIRST_COUNTERPARTY_DENOM" \ + --output deployment/juno-v1-testnet.json \ + --render +python3 scripts/check_juno_v1_frontend_config.py deployment/juno-v1-testnet.json +``` + +State checks: + +```sh +/opt/data/bin/junod query wasm contract-state smart "$ADDR_FACTORY" '{"config":{}}' --node "$RPC" -o json | jq +/opt/data/bin/junod query wasm contract-state smart "$ADDR_NATIVE_COIN_REGISTRY" '{"native_tokens":{"limit":30}}' --node "$RPC" -o json | jq +/opt/data/bin/junod query wasm contract-state smart "$ADDR_FACTORY" '{"pairs":{"limit":30}}' --node "$RPC" -o json | jq +/opt/data/bin/junod query wasm contract-state smart "$PAIR_ADDR" '{"pool":{}}' --node "$RPC" -o json | jq +/opt/data/bin/junod query wasm contract-state smart "$PAIR_ADDR" '{"simulation":{"offer_asset":{"info":{"native_token":{"denom":"ujuno"}},"amount":"1000000"}}}' --node "$RPC" -o json | jq +/opt/data/bin/junod query wasm contract-state smart "$ADDR_ROUTER" '{"config":{}}' --node "$RPC" -o json | jq +``` + +Launch is not ready until factory `pairs`, pair `pool`, and pair `simulation` all return sensible values for at least one seeded XYK pool. + +## 7. Safety checks before public launch + +- Artifact set exactly matches the eight v1 wasms; no stable/PCL/maker/staking/vesting/xASTRO/converter artifacts. +- Every wasm passes `cosmwasm-check` with Juno capabilities and no neutron capability requirement. +- Mainnet `wasm params` are re-queried immediately before broadcast; current review state is open upload/instantiate, but if `wasm store` becomes permissioned, upload path must be governance/authorized uploader, not a hot wallet assumption. +- Owner/guardian/treasury are DAO-approved and documented. +- Factory instantiate/update txs are reviewed for no stable/PCL pair configs and no surprise maker fee. +- Native registry has correct decimals for `ujuno` and each launch denom. +- First pool denoms are verified through IBC denom trace and wallet/explorer display. +- UI registry/config has no placeholders and passes strict guards. +- Seed liquidity amount is intentional and publicly acceptable; do not list thin/empty pools. +- One direct swap smoke test and one add/remove liquidity smoke test are executed on the target chain before public comms. +- Risk notice is published: experimental Juno DEX v1, thin liquidity, verify contracts. + +## 8. Rollback/freeze risks + +- `wasm store` code IDs are immutable. Rollback means stop using a bad code ID, upload fixed code, migrate only contracts that support safe migrate, or instantiate replacements. +- Bad factory config can block or misroute pool creation. Immediate freeze: execute factory `update_pair_config` with `is_disabled=true` for XYK, or remove/avoid listing the factory in the frontend registry. +- Bad incentives config can create reward accounting confusion. Immediate freeze: hide incentives in UI and update factory `generator_address` to `null` if needed. +- Bad pool cannot be deleted from chain history. Mitigation: remove from registry/UI, publish warning, create replacement pool, and do not seed more liquidity. +- Bad native denom decimals corrupt display/quotes. Freeze affected denom in registry/UI until native coin registry and frontend config agree. +- Router misconfiguration affects multi-hop only. Keep direct pair swaps as launch path; hide router routes until router `config` and simulations pass. +- Mainnet upload permissioning can block deployment entirely if chain params change. Current review query shows `code_upload_access.permission=Everybody`; re-query before broadcast and require a governance/authorized uploader path only if the target query says code upload is restricted. + +## 9. Exact current blockers + +1. Optimized artifacts have not been produced in this environment because Docker daemon is unreachable. +2. Real code IDs do not exist in this repo yet for the eight Astroport-Juno v1 artifacts plus cw20-base. +3. Real instantiated contract addresses do not exist in this repo yet. +4. Owner, guardian, treasury, target upload signer, and first launch counterpart denom are not finalized in repo state. +5. The factory/incentives circular address dependency must be resolved by the update-after-incentives path above or by a documented Instantiate2/precomputed-address path. +6. No seeded XYK pool has been verified by factory `pairs`, pair `pool`, and pair `simulation` queries. +7. No real direct swap or add/remove liquidity smoke tx has been executed on the target chain. + +## 10. Operator-ready acceptance gate + +DEX v1 deployment handoff is ready for frontend only when this exact command sequence passes against real target-chain outputs: + +```sh +python3 scripts/check_juno_v1_artifacts.py artifacts +python3 scripts/check_juno_v1_deployment_template.py deployment/juno-v1-testnet.json +python3 scripts/check_juno_v1_frontend_config.py deployment/juno-v1-testnet.json +/opt/data/bin/junod query wasm contract-state smart "$ADDR_FACTORY" '{"config":{}}' --node "$RPC" -o json +/opt/data/bin/junod query wasm contract-state smart "$ADDR_FACTORY" '{"pairs":{"limit":30}}' --node "$RPC" -o json +/opt/data/bin/junod query wasm contract-state smart "$PAIR_ADDR" '{"pool":{}}' --node "$RPC" -o json +/opt/data/bin/junod query wasm contract-state smart "$PAIR_ADDR" '{"simulation":{"offer_asset":{"info":{"native_token":{"denom":"'$DENOM'"}},"amount":"1000000"}}}' --node "$RPC" -o json +``` + +Until then, frontend work may use the placeholder template/read-only shell, but it must not present Juno DEX v1 as live. diff --git a/deployment/juno-v1-testnet.template.json b/deployment/juno-v1-testnet.template.json new file mode 100644 index 000000000..1d37b8153 --- /dev/null +++ b/deployment/juno-v1-testnet.template.json @@ -0,0 +1,150 @@ +{ + "network": { + "chain_id": "uni-7", + "bech32_prefix": "juno", + "fee_denom": "ujunox", + "native_asset_denom": "ujunox" + }, + "accounts": { + "owner": "juno1replaceowner000000000000000000000000000000000", + "guardian": "juno1replaceguardian000000000000000000000000000000", + "treasury": "juno1replacetreasury0000000000000000000000000000", + "tokenfactory_module": "juno1replacefactorymodule0000000000000000000000000" + }, + "code_ids": { + "astroport-factory": 0, + "astroport-incentives": 0, + "astroport-native-coin-registry": 0, + "astroport-oracle": 0, + "astroport-pair": 0, + "astroport-router": 0, + "astroport-tokenfactory-tracker": 0, + "astroport-whitelist": 0, + "cw20-base": 0 + }, + "addresses": { + "astroport-factory": "juno1replacefactory000000000000000000000000000000", + "astroport-incentives": "juno1replaceincentives00000000000000000000000000", + "astroport-native-coin-registry": "juno1replacecoinregistry00000000000000000000000", + "astroport-oracle": "juno1replaceoracle0000000000000000000000000000000", + "astroport-router": "juno1replacerouter000000000000000000000000000000", + "astroport-tokenfactory-tracker": "juno1replacetracker0000000000000000000000000000", + "astroport-whitelist": "juno1replacewhitelist00000000000000000000000000" + }, + "instantiate_msgs": { + "astroport-native-coin-registry": { + "owner": "juno1replaceowner000000000000000000000000000000000" + }, + "astroport-whitelist": { + "admins": [ + "juno1replaceowner000000000000000000000000000000000" + ], + "mutable": true + }, + "astroport-tokenfactory-tracker": { + "tokenfactory_module_address": "juno1replacefactorymodule0000000000000000000000000", + "track_over_seconds": true, + "tracked_denom": "factory/juno1replacefactory000000000000000000000000000000/astroport/share" + }, + "astroport-factory": { + "coin_registry_address": "juno1replacecoinregistry00000000000000000000000", + "fee_address": "juno1replacetreasury0000000000000000000000000000", + "generator_address": null, + "owner": "juno1replaceowner000000000000000000000000000000000", + "pair_configs": [ + { + "code_id": 0, + "is_disabled": false, + "is_generator_disabled": false, + "maker_fee_bps": 0, + "pair_type": { + "xyk": {} + }, + "permissioned": true, + "total_fee_bps": 30, + "whitelist": null + } + ], + "token_code_id": 0, + "tracker_config": { + "code_id": 0, + "token_factory_addr": "juno1replacefactorymodule0000000000000000000000000" + }, + "whitelist_code_id": 0 + }, + "astroport-router": { + "astroport_factory": "juno1replacefactory000000000000000000000000000000" + }, + "astroport-incentives": { + "reward_token": { + "native_token": { + "denom": "ujunox" + } + }, + "factory": "juno1replacefactory000000000000000000000000000000", + "guardian": "juno1replaceguardian000000000000000000000000000000", + "incentivization_fee_info": null, + "owner": "juno1replaceowner000000000000000000000000000000000" + }, + "astroport-oracle": { + "asset_infos": [ + { + "native_token": { + "denom": "ujunox" + } + } + ], + "factory_contract": "juno1replacefactory000000000000000000000000000000" + } + }, + "post_update_state": { + "astroport-factory": { + "generator_address": "juno1replaceincentives00000000000000000000000000", + "pair_configs": [ + { + "code_id": 0, + "is_disabled": false, + "is_generator_disabled": false, + "maker_fee_bps": 0, + "pair_type": { + "xyk": {} + }, + "permissioned": false, + "total_fee_bps": 30, + "whitelist": null + } + ], + "first_pool_launch_gate": "Keep factory XYK pair creation permissioned until the official pair exists, seed liquidity is confirmed non-zero, and smoke checks pass. Only then execute update_pair_config to set permissioned=false." + } + }, + "pair_create_msg_template": { + "pair_type": { + "xyk": {} + }, + "asset_infos": [ + { + "native_token": { + "denom": "ujunox" + } + }, + { + "native_token": { + "denom": "ibc/REPLACE_COUNTERPARTY_DENOM_HASH" + } + } + ], + "init_params": null + }, + "frontend": { + "required_addresses": [ + "astroport-factory", + "astroport-router", + "astroport-native-coin-registry", + "astroport-incentives" + ], + "optional_addresses": [ + "astroport-oracle" + ], + "pair_discovery": "query astroport-factory pairs/pair; do not hardcode pools before launch" + } +} diff --git a/deployment/operator-tx-checklist.md b/deployment/operator-tx-checklist.md new file mode 100644 index 000000000..387e83ec1 --- /dev/null +++ b/deployment/operator-tx-checklist.md @@ -0,0 +1,112 @@ +# Astroport-Juno v1 operator tx checklist + +Date: 2026-06-29T06:21:18Z + +Purpose: make the uni-7 DeFi v1 deployment handoff boring. This checklist names the exact transaction JSON files an operator should save, the values each file must yield, and the single command that converts them into the deployment config fill command. + +## Save these 16 tx JSON files + +Run store/instantiate commands with `--output json`, then save the full tx response bodies under an ignored local directory such as `deployment/tx/uni-7/`. Do not paste mnemonics, keyring output, or private material into these files. + +### Store txs → 9 code IDs + +| File | Extracted config key | +| --- | --- | +| `deployment/tx/uni-7/store-astroport-factory.json` | `code_ids.astroport-factory` | +| `deployment/tx/uni-7/store-astroport-incentives.json` | `code_ids.astroport-incentives` | +| `deployment/tx/uni-7/store-astroport-native-coin-registry.json` | `code_ids.astroport-native-coin-registry` | +| `deployment/tx/uni-7/store-astroport-oracle.json` | `code_ids.astroport-oracle` | +| `deployment/tx/uni-7/store-astroport-pair.json` | `code_ids.astroport-pair` | +| `deployment/tx/uni-7/store-astroport-router.json` | `code_ids.astroport-router` | +| `deployment/tx/uni-7/store-astroport-tokenfactory-tracker.json` | `code_ids.astroport-tokenfactory-tracker` | +| `deployment/tx/uni-7/store-astroport-whitelist.json` | `code_ids.astroport-whitelist` | +| `deployment/tx/uni-7/store-cw20-base.json` | `code_ids.cw20-base` | + +### Instantiate txs → 7 contract addresses + +| File | Extracted config key | +| --- | --- | +| `deployment/tx/uni-7/instantiate-astroport-factory.json` | `addresses.astroport-factory` | +| `deployment/tx/uni-7/instantiate-astroport-incentives.json` | `addresses.astroport-incentives` | +| `deployment/tx/uni-7/instantiate-astroport-native-coin-registry.json` | `addresses.astroport-native-coin-registry` | +| `deployment/tx/uni-7/instantiate-astroport-oracle.json` | `addresses.astroport-oracle` | +| `deployment/tx/uni-7/instantiate-astroport-router.json` | `addresses.astroport-router` | +| `deployment/tx/uni-7/instantiate-astroport-tokenfactory-tracker.json` | `addresses.astroport-tokenfactory-tracker` | +| `deployment/tx/uni-7/instantiate-astroport-whitelist.json` | `addresses.astroport-whitelist` | + +## Manual values to decide before rendering + +Set these as environment variables from the actual deployment plan: + +```sh +export JUNO_OWNER='juno...' +export JUNO_GUARDIAN='juno...' +export JUNO_TREASURY='juno...' +export JUNO_TOKENFACTORY_MODULE='juno...' +export FIRST_COUNTERPARTY_DENOM='ibc/...' +``` + +Keep v1 narrow: XYK pools, swaps, liquidity, native-denom incentives plumbing. No stable pools, LSTs, perps, yield theater, or new token scope. + +## Build `tx-sets.txt` + +```sh +mkdir -p deployment/tx/uni-7 +python3 scripts/extract_juno_v1_tx_sets.py \ + --code-id astroport-factory=deployment/tx/uni-7/store-astroport-factory.json \ + --code-id astroport-incentives=deployment/tx/uni-7/store-astroport-incentives.json \ + --code-id astroport-native-coin-registry=deployment/tx/uni-7/store-astroport-native-coin-registry.json \ + --code-id astroport-oracle=deployment/tx/uni-7/store-astroport-oracle.json \ + --code-id astroport-pair=deployment/tx/uni-7/store-astroport-pair.json \ + --code-id astroport-router=deployment/tx/uni-7/store-astroport-router.json \ + --code-id astroport-tokenfactory-tracker=deployment/tx/uni-7/store-astroport-tokenfactory-tracker.json \ + --code-id astroport-whitelist=deployment/tx/uni-7/store-astroport-whitelist.json \ + --code-id cw20-base=deployment/tx/uni-7/store-cw20-base.json \ + --address astroport-factory=deployment/tx/uni-7/instantiate-astroport-factory.json \ + --address astroport-incentives=deployment/tx/uni-7/instantiate-astroport-incentives.json \ + --address astroport-native-coin-registry=deployment/tx/uni-7/instantiate-astroport-native-coin-registry.json \ + --address astroport-oracle=deployment/tx/uni-7/instantiate-astroport-oracle.json \ + --address astroport-router=deployment/tx/uni-7/instantiate-astroport-router.json \ + --address astroport-tokenfactory-tracker=deployment/tx/uni-7/instantiate-astroport-tokenfactory-tracker.json \ + --address astroport-whitelist=deployment/tx/uni-7/instantiate-astroport-whitelist.json \ + > deployment/tx/uni-7/tx-sets.txt +``` + +Quick sanity check before rendering: + +```sh +wc -l deployment/tx/uni-7/tx-sets.txt +python3 scripts/extract_juno_v1_tx_sets.py --scan deployment/tx/uni-7/*.json +``` + +Expected: `tx-sets.txt` has 16 non-empty `--set ...` lines, each mapped file scans to exactly one relevant code ID or address, and no unrelated contract addresses leak into a mapped tx file. + +## Render and validate final config + +```sh +python3 scripts/build_juno_v1_deployment_command.py \ + --tx-sets deployment/tx/uni-7/tx-sets.txt \ + --owner "$JUNO_OWNER" \ + --guardian "$JUNO_GUARDIAN" \ + --treasury "$JUNO_TREASURY" \ + --tokenfactory-module "$JUNO_TOKENFACTORY_MODULE" \ + --counterparty-denom "$FIRST_COUNTERPARTY_DENOM" \ + --output deployment/juno-v1-testnet.json \ + --render +``` + +The final green line should include: + +```text +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +``` + +## If extraction fails + +- `missing tx JSON`: save the full `junod` tx response body at the expected path. +- `no code_id found` or `no contract address found`: run `--scan` against the file and confirm it is the final committed tx response, not an unsigned tx or broadcast stub. +- `multiple code_id` or `multiple contract addresses`: split the operation into one tx JSON per config key, or inspect manually before mapping. +- `tx sets missing required deployment values`: compare `deployment/tx/uni-7/tx-sets.txt` against the 16-row list above. + +Forward. The DEX v1 handoff should be mechanical, not mystical. diff --git a/deployment/records/README.md b/deployment/records/README.md new file mode 100644 index 000000000..a83f2e7b6 --- /dev/null +++ b/deployment/records/README.md @@ -0,0 +1,3 @@ +# Deployment records + +Human-readable records for real Juno deployment outputs. These files are public summaries; raw tx JSON and wasm binaries should stay in operator archives unless a release process explicitly publishes them. diff --git a/deployment/records/juno-v1-mainnet-deployment-2026-07-01.md b/deployment/records/juno-v1-mainnet-deployment-2026-07-01.md new file mode 100644 index 000000000..ed16e7f33 --- /dev/null +++ b/deployment/records/juno-v1-mainnet-deployment-2026-07-01.md @@ -0,0 +1,74 @@ +# Astroport-Juno v1 mainnet deployment — 2026-07-01 + +## Scope + +Experimental Astroport-Juno v1 deployment on `juno-1`, using the Juno agent hot wallet as upload signer/owner/guardian/treasury for thin-liquidity testing. Product surface remains XYK-only: no DEX token, stablecoin, LST, perps, yield vault, PCL/stable pairs, staking/maker/vesting/xASTRO launch surface. + +## Contracts + +| Component | Address / Code ID | +|---|---| +| Factory | `juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca` / code `5129` | +| Pair code | code `5133` | +| First pair | `juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv` | +| Native coin registry | `juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2` / code `5131` | +| Router | `juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s` / code `5134` | +| Incentives | `juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598` / code `5130` | +| Oracle | `juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p` / code `5132` | +| Whitelist code | code `5136` | +| Tokenfactory tracker code | code `5135` | +| cw20-base | existing code `109` | + +## First test pool + +- Pair: `juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv` +- Assets: `ujuno` / `factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323` +- LP denom: `factory/juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv/astroport/share` +- Pair creation was opened publicly after pool verification (`permissioned=false`). + +## Key txs + +| Action | Tx | +|---|---| +| Store factory | `9F554BF49C45C3250D6BF0EEEEB446EA16AC9FF9C6E6CC1866912F2DC537A57F` | +| Store pair | `FD5FDF8D541858BC586D3F6705A4B79C0DC4D7AEE4D7C9305067C298AB617735` | +| Create test denom | `0C92541E989029A87F20C1768CFF59A1A570B01C356C25DA9139A201BDABA928` | +| Mint test denom | `5370935A3C98B8B36281C4BAD87F528269961B89E7F9B29015AD713FD11133D2` | +| Instantiate factory | `56B88238DD314A4774CC9019CE6763FE57A2FEBDE93F7DE85BD5F97F921C02AE` | +| Instantiate incentives | `7F4C3F4722BF41C6C832387F401ABBFA4F3F52B6B3DE77D75496BB7B0CEA8BAD` | +| Instantiate router | `39F331EF10486231BDB2C851F763EA826E3B19ABA04ED7D7B28FA95EC8666219` | +| Create pair | `8EFD15276286C15D5CFF11B55D49522D2987E16F8220DE671CA0971E586BCD8E` | +| Seed liquidity | `DEE44565B5E6124A27430646A371691396A504497EB0315D4A66675C8C765401` | +| Open pair creation | `5A214C0FAE998A5A772A13EA407C267E57787276A34AD8B7D4AF91FFD866E35E` | +| Smoke swap | `15CE5277D55668B4ADE7D44132C7E2EE4FE71882B2003C28503AF09D7138B502` | +| Smoke add liquidity | `F6C33B16578AAA1A4DC4931C250A07649F0F750AC8EAAE150146F5C6D54C5079` | +| Smoke withdraw liquidity | `ED1923D1DF041245296358BEC1EF80FDEFF47A3118F8CBD06EB73A7F5E860E97` | + +## Verification + +- Artifacts built locally with Rust 1.81.0 contract-by-contract, optimized with `wasm-opt -Oz`. +- `scripts/check_juno_v1_artifacts.py artifacts` passed. +- `cosmwasm-check` passed for all 8 artifacts. +- Factory pair query, pair pool query, pair simulation query, router config query, and native coin registry query all passed. +- Smoke swap/add-liquidity/withdraw-liquidity txs all included successfully. +- Frontend registry PR: https://github.com/JakeHartnell/juno-website/pull/1 (`aad48842f7b07c3f42842b9aa3db613419071caf`). +- Durable local artifacts/tx evidence: `/opt/data/repos/astroport-juno-v1-mainnet-deploy-20260701/`. + +## Recommendation + +Launch blocker is cleared for a test/preview frontend. Still label the pool as thin-liquidity experimental and do not imply public liquidity recommendation. Consider transferring owner/admin roles from the hot wallet to DAO-controlled governance before broader public promotion. + +## Artifact SHA-256 + +```text +e38c3a9490fabe469605d2814fcf6e79b1482d031a66cd8dfdc23a010afc885d artifacts/astroport_factory.wasm +d057f063573b7974113aaeee27c3252c075fdc11e2dd31c3485d11ca0d3ec9e3 artifacts/astroport_incentives.wasm +b7bba9d965a2e5074b29c9a7c08782da86535c336239203a8f6ba5213d7c3b0f artifacts/astroport_native_coin_registry.wasm +a9062ec7d40ddfa7fac16f2da826c8dcbe887c9cf6d62aa85d1e939029d45066 artifacts/astroport_oracle.wasm +a99f1b1b3b3bed72ed9c4bdf16adfa51fc90057a943d31bb1c5ac870c9c95249 artifacts/astroport_pair.wasm +e5f9982127fbfe698958172f97f68483152e26d4385f381e9056ee62ce19f3c6 artifacts/astroport_router.wasm +a77ccfaf87f4dc3ce39c09980cf3783bf15e2f818e5250bcd411bbbb934609ee artifacts/astroport_tokenfactory_tracker.wasm +aff60f2783e55a766f671506ad0cb32af67b5b166c54dc51ad44120e278e0de1 artifacts/astroport_whitelist.wasm +``` + +Full local tx JSON + wasm artifact archive exists at `/opt/data/repos/astroport-juno-v1-mainnet-deploy-20260701/` in the Juno agent environment. Do not commit raw tx directories or wasm binaries unless a release process explicitly calls for them. diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 000000000..635f6889c --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,13 @@ +VITE_DEX_RPC_URL=https://juno-rpc.kleomedes.network +VITE_DEX_REST_URL=https://juno-api.polkachu.com +VITE_DEX_EXPLORER_URL=https://www.mintscan.io/juno +VITE_DEX_INDEXER_URL=http://localhost:8787 +VITE_DEX_INDEXER_DISABLED=false +VITE_DEX_INDEXER_TIMEOUT_MS=2500 +VITE_DEX_INDEXER_RETRY=1 +VITE_DEX_INDEXER_STALE_AFTER_MS=120000 +VITE_DEX_INDEXER_CIRCUIT_BREAKER_MS=60000 + +# Optional: enables WalletConnect/mobile wallet adapters in CosmosKit. +# Browser extension wallets remain available when this is omitted. +VITE_WALLETCONNECT_PROJECT_ID= diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..d7c89f78f --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,6 @@ +dist/ +tsconfig.tsbuildinfo +node_modules/ + +.env +.env.local diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..3e9d68bf5 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,86 @@ +# Juno DEX Frontend + +From-scratch Vite/React/TypeScript frontend for the in-repo Juno DEX deployment lane. + +## Commands + +```sh +npm install +npm run codegen +npm run typecheck +npm test +npm run build +npm run dev +``` + +## Contract code generation + +Typed contract clients, message composers, and schema types are generated with `@cosmwasm/ts-codegen` into `src/lib/generated/`. Regenerate them after schema changes with: + +```sh +npm run codegen +``` + +The generator reads the committed schema directories under `../schemas/astroport-*/raw/` and currently emits SDKs for factory, pair, router, incentives, oracle, and native-coin-registry. Pair stable and pair concentrated clients will be emitted automatically once their schema directories are committed. + +## Scope + +V1 is intentionally narrow: a Juno-native read-only DEX surface with Keplr connection, one strict preview pool registry, pool reserve queries, direct pair quote queries, and transaction hooks for direct swap/provide/withdraw flows. The first TokenFactory counterparty is marked preview/test and thin-liquidity until launch assets and ownership posture are confirmed. + +## Theme and design system + +The frontend is wrapped in `@interchain-ui/react`'s `ThemeProvider` from `src/main.tsx` with `themeMode="dark"`; there is intentionally no light-mode surface. Juno brand tokens live in `src/theme/junoTheme.ts` and are the source of truth for the app palette, radii, spacing, typography, and shadows. + +- `interchainJunoTheme` maps the Juno palette into Interchain UI theme variables. +- `junoCssVars` exposes the same tokens as CSS custom properties for legacy/local surfaces that Interchain UI primitives do not cover yet. +- `src/styles/theme.css` should stay thin and reference those variables rather than hard-coded colors. +- Brand artwork is local, original SVG under `src/assets/`; do not fetch or commit third-party/copyrighted logo files unless their license is explicit. + +New UI work should prefer Interchain UI primitives (`Box`, `Stack`, `Text`, `Button`, etc.) and use local CSS only for DEX-specific composition. + +## Registry rules + +`src/data/registry.juno-1.json` is the source for the app shell. `src/config/registry.ts` validates that it is `juno-1`, contains only enabled XYK pools, has real-looking Juno addresses, has explorer links, and does not contain placeholder strings. + +## Runtime configuration + +The static build reads public Vite variables at build time. Copy `.env.example` to `.env.local` for local overrides, and set the same names in the static host for preview and production environments: + +| Variable | Purpose | +| --- | --- | +| `VITE_DEX_RPC_URL` | Public Juno RPC endpoint shown in status and used by on-chain reads. | +| `VITE_DEX_REST_URL` | Public Juno REST/LCD endpoint used for smart queries. | +| `VITE_DEX_EXPLORER_URL` | Explorer base URL for contract/account links. | +| `VITE_DEX_INDEXER_URL` | Stable HTTPS indexer/API origin consumed by analytics, candles, and wallet history. | +| `VITE_DEX_INDEXER_DISABLED` | Set `true` to force on-chain fallback while keeping the UI deployable. | +| `VITE_DEX_INDEXER_TIMEOUT_MS`, `VITE_DEX_INDEXER_RETRY`, `VITE_DEX_INDEXER_STALE_AFTER_MS`, `VITE_DEX_INDEXER_CIRCUIT_BREAKER_MS` | Client-side indexer resilience knobs. | + +`VITE_DEX_RPC_URL`, `VITE_DEX_REST_URL`, and `VITE_DEX_EXPLORER_URL` override the committed registry endpoints during `npm run build`; contract addresses and pool metadata still come from `src/data/registry.juno-1.json`. + +## Static hosting and CI/CD + +The frontend is configured for Vercel static hosting via `vercel.json`: + +- Pull requests receive Vercel preview deployments when the repository or Vercel Git integration is enabled. +- Pushes/merges to `main` deploy the production frontend. +- The GitHub Actions deployment workflow is gated by repository variable `VERCEL_ENABLED=true` and uses `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` secrets. Keep API keys and deployment tokens in GitHub/Vercel secrets only. +- Vercel project root should be `frontend`, install command `npm ci`, build command `npm run build`, output directory `dist`. + +Recommended release flow: + +1. Open a PR and wait for `Frontend CI` (`npm ci`, typecheck, lint, unit tests, E2E, build) and the Vercel preview to pass. +2. Verify the preview points at the intended preview indexer URL and that indexer-dependent panels gracefully fall back if `/health` is unavailable. +3. Set production Vercel env vars, especially `VITE_DEX_INDEXER_URL`, to the stable indexer/API HTTPS URL before merging. +4. Merge to `main`; the production deploy should run automatically. +5. Smoke-check the production domain over TLS: app loads, wallet connection opens, RPC status reports a Juno block, and the configured indexer `/health` returns `status: ok` or UI fallback is intentional. + +## Domain and TLS + +Use the static host's managed TLS. Point the production DNS record at the host target only after a successful preview/prod deployment, then verify: + +```sh +curl -I https:/// +curl -fsS "$VITE_DEX_INDEXER_URL/health" +``` + +Do not commit DNS provider credentials, Vercel tokens, or private deployment material. diff --git a/frontend/docs/a11y-performance.md b/frontend/docs/a11y-performance.md new file mode 100644 index 000000000..c8d375bdd --- /dev/null +++ b/frontend/docs/a11y-performance.md @@ -0,0 +1,38 @@ +# Accessibility and performance checks + +This pass keeps the quality tooling lightweight and tied to the existing Vite/Playwright setup. + +## Automated accessibility smoke test + +Run: + +```sh +PLAYWRIGHT_BROWSERS_PATH=/opt/data/.cache/ms-playwright npm run test:a11y +``` + +The Playwright test builds the e2e bundle, serves it with `vite preview`, and runs axe against the core routes (`/swap`, `/pools`, `/portfolio`, `/create`, `/stats`) plus the swap token selector dialog. The assertion is intentionally scoped to the issue acceptance criterion: no **critical** axe violations. + +## Lighthouse performance spot check + +Run against the built e2e preview server: + +```sh +npm run build:e2e +npm run preview:e2e -- --host 127.0.0.1 --port 4173 +npx lighthouse http://127.0.0.1:4173/swap \ + --quiet \ + --chrome-flags="--headless --no-sandbox" \ + --only-categories=performance,accessibility \ + --output=json \ + --output-path=./lighthouse-swap.json +``` + +For issue #52 evidence, include the performance/accessibility category scores and keep `lighthouse-swap.json` out of git unless a future CI job consumes it. + +## What this pass covers + +- Dialog focus is trapped, Escape closes modals, and focus returns to the invoking control. +- Token selector controls expose explicit accessible names, expanded state, search/result relationships, and keyboard-friendly native buttons. +- Reduced-motion users do not receive the shimmer animation or long transitions. +- Route components are lazily loaded so non-swap pages do not inflate the initial swap route chunk. +- Read-only Stargate RPC access is cached so wallet balance refreshes reuse a single client connection instead of reconnecting on every query. diff --git a/frontend/docs/ux-review.md b/frontend/docs/ux-review.md new file mode 100644 index 000000000..db66ba7f5 --- /dev/null +++ b/frontend/docs/ux-review.md @@ -0,0 +1,399 @@ +# Juno DEX: UX Improvement Plan + +## 1. Diagnosis + +Three structural causes, not forty small ones. + +**(a) There is no progressive disclosure anywhere.** Every fact the app knows is rendered permanently, at the same visual weight, in the flow of the page. `QuoteCard.tsx:94-155` is a six-row telemetry table that never collapses. `PoolDetailPage.tsx:37-126` is ten stacked full-width sections. `DexShell.tsx:96-103` pins a five-row network diagnostics table into the chrome. Uniswap's swap card has one always-visible line (the rate) and one chevron; ours has six rows, then up to three consent checkboxes, then a badge strip, then a duplicate red error line, then the button. + +**(b) Every concept has two-to-four owners, and nobody deleted the old one.** Slippage is settable in two places and displayed in four (`QuoteCard.tsx:122-148` + `SettingsPanel.tsx:40-81` + `SwapForm.tsx:295-298` + `AddLiquidityForm.tsx:198`, the last one being a button with no `onClick`). One transaction fires a toast AND an inline `TxStatusDialog` card AND a TransactionCenter row (`useTxRunner.tsx:110-164`). Pool detail renders the same three action buttons twice (`LpPositionPanel.tsx:100-104` + `PoolDetailPage.tsx:51-59`) and the same risk badges twice (`PoolDetailPage.tsx:44` + `:67`). `/liquidity` is a worse `/portfolio`. `/stats` is dead. Six of the seven reviewer lenses independently landed on this as the top complaint. + +**(c) The styling layer is four overlapping half-systems with no scale.** `junoTokens` -> a dead `--juno-*` namespace (44 of 87 vars have zero consumers) -> a retrofitted short-alias namespace that CSS actually reads -> Interchain UI's own theme. On top of that: ~32 distinct font sizes, 18 gap values, 7 card paddings, an **inverted elevation ladder** (cards at `#230A0C` are *darker* than the `#270B0D` canvas they sit on, while carrying an 18px/40px drop shadow), coral used as brand AND accent AND link AND error, 81 `!important` flags fighting Interchain UI, and two undefined vars (`--ease-mech`) that silently kill five transitions. + +Fix those three and it stops reading as ugly. Everything else in this document is a consequence. + +--- + +## 2. The Swap page + +### Current structure + +``` ++---------------------------- 980px, centered ---------------------------+ +| [ 440px fixed ] | [ flex ] | +| SWAP CARD | PriceCandleChart (compact) | +| h2 "Swap" [gear] | ---------------------------------| +| "Sell exact · enter the amount | "Market activity" | +| you want to send" | h3 "Recent transactions" | +| [ You send | TOKEN v ] | "Last 10" | +| (flip) | hand-rolled .transaction-list | +| [ You receive| TOKEN v ] | (10 rows, forked markup) | +| "Target output is an estimate…" | | +| +--- QuoteCard (always open) --+ | | +| | Rate | | | +| | Route | | | +| | Price impact | | | +| | Minimum received | | | +| | Max slippage [.1][.5][1.0] | | <- 2nd slippage control | +| | Quote status: expires in 27s | | <- 1s interval, re-renders form | +| +------------------------------+ | | +| [!] elevated impact notice | | +| [x] I understand high impact | | +| [!] extreme impact BLOCKED | | +| [x] I understand impact unavail. | | +| [x] I understand high slippage | | +| RiskBadgeList (fires on the | | +| happy path: "Verified · XYK") | | +| [x] RiskAcknowledgement | | +| red: "Enter amount" | <- duplicate of button label | +| [ Enter amount ] | | +| TxStatusDialog (never clears) | | ++------------------------------------+-----------------------------------+ +``` + +### Proposed structure + +``` ++---------------------------- 980px, centered ---------------------------+ +| [ 440px ] | [ flex ] | +| SWAP CARD (raised, real card) | PriceCandleChart | +| Swap [0.5% ⚙] | (keep. flatten its surface so | +| | the swap card carries weight) | +| +------------------------------+ | | +| | 12.5 [JUNO v] | | | +| | $41.20 Balance: 18.2 | | <- balance IS the max button | +| +------------------------------+ | | +| (flip) | | +| +------------------------------+ | | +| | 523.11 [USDC v] | | | +| | $41.05 | | | +| +------------------------------+ | | +| | | +| 1 JUNO = 0.0421 USDC (v) | <- one line + chevron | +| > Minimum received 520.4 | | +| > Price impact 0.12% | | +| > Max slippage 0.50% | | +| > Route 2 hops | | +| | | +| [!] ONE hazard strip, ONE box: | <- only when hazards exist | +| - high price impact (8.2%) | | +| - unverified asset FOO | | +| [x] I understand and accept | | +| | | +| [ Review swap ] | <- the button IS the state | ++------------------------------------+-----------------------------------+ +``` + +### Deletions and merges + +| What | Where | Action | +|---|---|---| +| Duplicate validation text | `SwapForm.tsx:375` | **Delete.** `actionCopy` (`:186`) already ends in `validationError ?? "Review swap"`, so the button already says it. Keep `:374` (wrong-network explanation, mutually exclusive with `:375`). | +| Mode subtitle + exact-out notice | `SwapForm.tsx:286-289`, `:348` | **Delete both.** Exact-out is already signalled at `:335` (label flips to "Target receive"), `:317` (fiat hint), and `:381` (review description). Four announcements of one mode. Also delete `.swap-mode-copy` (`theme.css:925-931`). | +| QuoteCard 6-row table | `QuoteCard.tsx:94-155` | **Collapse.** Always-visible: `rateLabel` (`:53-71`) + chevron. Inside `
`: Minimum received, Price impact, Max slippage (read-only), Route. | +| QuoteCard slippage chips + `SLIPPAGE_PRESETS` const | `QuoteCard.tsx:8-12, 122-148` | **Delete.** Hardcoded duplicate of `SLIPPAGE_PRESET_BPS` (`lib/swap/slippage.ts:3`). Drop the `onSlippageBps` prop (`:24, :36`) and its pass at `SwapForm.tsx:349`. Slippage becomes settable in **exactly one** place: the gear. | +| "Quote status" countdown row | `QuoteCard.tsx:149-154` | **Delete.** Expiry is enforced at `SwapForm.tsx:111/149` and re-checked at sign time (`:257-264`). Worse: the 15s refetch resets the counter before the 30s TTL, so it ticks 30 -> 16 -> 30 forever and can never fire. | +| 1-second `setInterval` | `useSwapQuote.ts:93-96` | **Delete.** Its only consumer was the countdown. It re-renders the entire SwapForm every second, even with an empty form. Replace with a single `setTimeout` scheduled at `quoteUpdatedAt + TTL` that flips `isExpired` once. Keep `isExpired` (load-bearing at `SwapForm.tsx:111/149/259`). | +| Dead `updatedAt` prop | `QuoteCard.tsx:32`, passed at `SwapForm.tsx:349` | **Delete the prop only.** `quote.quoteUpdatedAt` stays; `SwapForm.tsx:263` needs it. | +| "Selected slippage" `
` | `SettingsPanel.tsx:79-81` | **Delete.** Restates the value the active preset button is already highlighting. | +| Raw contract param in tooltip | `SwapForm.tsx:298` | `title={`Swap max_spread ${maxSpread}`}` -> show `{formattedSlippagePercent}%` as a visible pill next to the gear. `max_spread` already lives in SettingsPanel's "Technical settings" `
` (`:85`). | +| Up to 3 stacked consent checkboxes | `SwapForm.tsx:350-373` | **Merge into one ``.** Note: `hasHighPriceImpact` (source=pair) and `hasUnavailablePriceImpact` (source=router) are mutually exclusive, so the true worst case is 3 checkboxes, not 5. Collapse the four ack booleans (`:84-87`) into one `hazardsAcknowledged` keyed on a stable `pendingHazards` id-join, which also **fixes a live bug**: today `slippageAcknowledged` does not reset when a *new* hazard appears from a route change, so a stale ack can survive the hazard set growing. Keep the extreme-impact hard block (`:359`) untouched. Do not delete the shared `RiskAcknowledgement` component (AddLiquidity/RemoveLiquidity/CreatePool still use it). | +| Elevated-impact notice card | `SwapForm.tsx:350-352` | **Delete.** `QuoteCard.tsx:45-51` already renders the Price impact row with `status-warn` for `severity === "warning"`. | +| Happy-path risk badges | `SwapForm.tsx:372`, `lib/risk.ts:127-142` | Pool-type badge severity -> `"info"` for all types (keep `requiresAcknowledgement` flag). Drop `caveated-liquidity-math` from `assessRouteRisk` (it describes provide/withdraw math, meaningless on a swap; keep it in `assessPoolRisk`). Then filter the SwapForm list to `severity === warning|danger` so a verified JUNO/USDC xyk swap shows **no badges at all**. This also stops info badges from eating the `max=4` slots and pushing a real hazard into the "+N" chip. | +| Forked transaction list | `SwapPage.tsx:38-52` | **Delete.** Hand-rolls `.transaction-list` markup while importing `formatAssetFlow`/`formatTimestamp` from `WalletTransactionHistory` (`:9`), and reimplements loading/error/empty as bare `

` tags. Pool activity has a canonical home at `PoolDetailPage.tsx:99-111`. `MarketPanel` reduces to the chart. | +| Token input = 3 stacked strips | `TokenAmountInput.tsx:69-129`, `swap.css:8-56` | **Collapse to 2 rows.** Row 1: amount + token pill. Row 2: left = USD value, right = `Balance: 12.5` as a **button** that applies max. Delete the absolutely-positioned topline (`swap.css:9-11, 16-23` uses `padding-top:38px` + `position:absolute` as a hack), the `onHalf`/`halfBaseAmount` props, and the `.token-amount-actions` MAX/50% row. Fold "MAX reserves 0.25 JUNO for network fees" into the max button's `title` and delete `SwapForm.tsx:316-317`. **Breaks 4 other consumers** (`AddLiquidityForm:210-213`, `RemoveLiquidityForm:148`, `IncentivesPanel:128`, `TokenAmountInput.test.tsx:57-72`) - budget for them. | +| No USD value anywhere | new `src/queries/usePrices.ts` | **Add** (the one addition on this page). Wrap `indexerClient.prices(assets)` (`lib/indexer/client.ts:76`) through the existing `indexerFallback` path. **Mandatory guard:** render nothing when `priceStatus` is `"missing"`/`"stale"` or `isPriceMock` is true (`indexer/types.ts:15,18`). Season 0 test tokens are live; never print a confident dollar figure from a mock price. | +| Unaddressable pair | `SwapPage.tsx:15` (`pools[0]`), `SwapForm.tsx:79-80` | **Add** `useSearchParams` (`grep` confirms zero uses app-wide, though react-router 7 is already a dep). Read `?from=&to=`, validate against `buildSelectableAssets(pools)`, reject unknown ids and `from === to`, write with `replace: true`. Then add a **Trade** link on `PoolDetailPage` and PoolTable rows. This is the funnel from pools into the flagship surface, which does not exist today. | +| TransactionReview: 12 rows | `SwapForm.tsx:385-395` | **Trim 9 rows -> 5.** Keep: You send, Receive, Minimum received, **Max slippage** (enforced on the message + tone carrier for high slippage), **Price impact** (tone carrier for danger). Delete: Route, Pool commission, "Assets" (a sentence), "Pool status" (a sentence). Fold pool label/status/verified into the existing per-hop `disclosures` labels (`:399`). **Do not** move Max slippage or Price impact into the `

` labelled "Contracts and identifiers" - burying a danger-toned row defeats the modal. | +| Flip button declared twice, 15 `!important` each | `theme.css:884-900` (dead) vs `swap.css:60-82` (wins) | **Delete the theme.css block**, drop every `!important` from the swap.css block. Also: `.swap-amount-stack` is already a grid; make it `grid-template-rows: auto 0 auto` and put the button in row 2 with `place-self:center; position:static`. Today it is absolutely positioned at 50%/50% of the whole stack, so the fiat-hint row on the send box pushes it visibly **below** the seam on the most common page load. | + +### Flow bugs on this page (not cosmetic) + +- **The Confirm button in the review modal disables itself ~15s after it opens.** `reviewIsCurrent` (`SwapForm.tsx:257-264`) compares `quote.quoteUpdatedAt === reviewSnapshot.updatedAt`, and TanStack bumps `dataUpdatedAt` on every 15s refetch **even when the quote is byte-identical**. The user reads the review for 15 seconds and gets a false "The amount, route, slippage, or quote version changed" alert with no in-modal escape. **Fix:** drop the timestamp equality; compare `route.id`, `offer_amount`, `return_amount`, `slippageBps`, and `!quote.isExpired`. Add a `warningAction` prop to `TransactionReview` wired to `handleReview` so the warning has a "Refresh and re-review" button. **Do not** suppress the poll while the snapshot is open: that just moves the trap to 30s and latches it. +- **The CTA is disabled and relabelled by every background refetch.** `balancesReady` (`:103`) and `quoteReady` (`:111`) both key off `isFetching`, which is true for background polls (balances 30s, quote 15s). The button reads "Review swap" and then flips to "Refreshing route…" and dies, roughly twice a minute, on a page where nothing is happening. **Fix:** gate on data presence, not fetch activity (`balances.data !== undefined`; `Boolean(quote.data) && !isDebouncing && !isError && !isExpired`). **Do not** add `placeholderData: keepPreviousData` - the amount/pair/mode are in the query key, so it would surface a stale quote as current. +- **Expired quote is a ~15s dead end.** After an alt-tab, `isExpired` disables the button with "Quote expired — refresh required" and there is no refresh control (`refreshQuote` exists at `useSwapQuote.ts:112` but is only reachable inside `handleReview`, which early-returns on `submitDisabled`). **Fix:** set `refetchOnWindowFocus: true` on this one query (overriding the global `false` at `main.tsx:25`), and make the residual expired state a *live* CTA: `needsQuoteRefresh` -> button reads "Refresh quote", stays enabled, calls `quote.refreshQuote()`. Keep it *below* sameToken / hasAmount / exceedsBalance in the priority chain. +- **The form does not reset after a confirmed swap.** `handleSwap` (`:266-279`) clears only `reviewSnapshot`. Pass a per-call `onSuccess` clearing `amount`/`askAmount`/`quoteMode`. **Not** `onSettled` - `runTx` rethrows on failure, so `onSettled` would wipe the input on wallet rejection and destroy the retry path. +- **Token selector has no keyboard completion.** No `onKeyDown` on the search input (`TokenSelect.tsx:127`): ArrowDown/Enter do nothing. Add `activeIndex` + arrow/Home/End/Enter, skipping `disabled` assets (`selectAsset` no-ops on them at `:100`), reset on `[query, isOpen]`. Do **not** add `aria-activedescendant` against `role="list"` (fake a11y), and do **not** `tabIndex={-1}` the favorite star (that removes keyboard access to favoriting). + +--- + +## 3. The Pool detail page + +### Current structure + +Ten stacked, equally-weighted, full-width sections. The three things a user came for (chart, my position, deposit form) are at positions 7, 2, and "behind a modal you have to find". + +``` +header (label, XYK · 30 bps, RiskBadgeList) :39-47 +LpPositionPanel [Add][Remove][Stake/claim] :49 <- buttons #1 +"Manage your position" [Add][Remove][Manage rewards] :51-59 <- buttons #2, SAME handlers +3x (add / remove / stake) :61-63 +"Pool status and risk" -> RiskBadgeList AGAIN + 1 line :65-70 <- badges #2 +"Performance" -> 3 MetricCards + "unavailable" line :72-83 +"Reserve composition" -> ReserveCards + "Current price":85-93 +PriceCandleChart :95-97 <- 7th +"Recent pool activity" :99-111 +
Technical pool details :113-125 +``` + +### Proposed structure + +``` ++------------------------------------------------------------------+ +| Pool · JUNO / USDC [← Back to pools] | +| JUNO/USDC | +| XYK · 30 bps TVL $1.2M 24h vol $84k APR 12.4% | +| [risk badges, max=6, only if not clean] updated 2m ago | ++------------------------------------------------------------------+ +| PRICE CHART (flush, no card chrome; price readout IS the heading) | +| | ++------------------------------------------------------------------+ +| YOUR POSITION (renders only when you have one) | +| 0.42% of pool · 1,204 LP · 512 JUNO + 21,004 USDC | +| [Add liquidity] [Remove] [Stake / claim] <- ONE row | ++------------------------------------------------------------------+ +| Pool reserves | +| [JUNO logo] 1,204,000 JUNO [USDC logo] 512,000 USDC | +| 1 JUNO ≈ 0.42 USDC (on-chain spot, survives indexer outage) | ++------------------------------------------------------------------+ +| Recent pool activity | ++------------------------------------------------------------------+ +| > Technical pool details | ++------------------------------------------------------------------+ +``` + +Section order: **header+stats -> chart -> your position (conditional) -> reserves -> activity -> technical.** The page is a single-column grid (`pools.css:149`), so the reorder is a pure JSX move; no CSS layout work. + +### Deletions + +| What | Where | Action | +|---|---|---| +| Duplicate action row | `PoolDetailPage.tsx:51-59` | **Delete the whole section.** `LpPositionPanel.tsx:100-104` already renders Add/Remove/Stake bound to the same `setManageAction` handlers. "Stake / claim" and "Manage rewards" are two names for one modal. Also delete `.manage-liquidity-actions` (`theme.css:1631-1649`). Keep `.lp-position-actions` (PortfolioPage uses it). | +| **Third** Add-liquidity button | `LpPositionPanel.tsx:64` | The quick-actions row (`:100-104`) renders unconditionally, *outside* the loading/error/empty/position branches. So a connected wallet with no LP sees three "Add liquidity" buttons in one viewport. Drop the `action` prop from the EmptyState. | +| Duplicate risk badges | `PoolDetailPage.tsx:65-70` | **Delete the "Pool status and risk" section.** Bump the header list (`:44`) to `max={6}` so nothing is lost. Move the `reserves.isError` note down to the reserves section where the stale numbers actually appear. The unverified sentence at `:68` is redundant with the badge's own `title` (`lib/risk.ts:109-110` already sets that copy as `description`, and `RiskBadges.tsx:11` already renders it as `title`). **Leave** `AddLiquidityForm.tsx:196` / `RemoveLiquidityForm.tsx:140` alone: they render inside modals that overlay the page, and `RiskAcknowledgement` returns null for a *blocked* pool, so removing them leaves the worst case with zero risk context. | +| Empty position card in the prime slot | `LpPositionPanel.tsx` + `PoolDetailPage.tsx:49` | Add `hideWhenEmpty?: boolean` (default false, so `LiquidityPage.tsx:32` is unaffected). Return null when connected + not loading + no position, and when disconnected. Keep rendering on loading/error. In `compact`, also drop the restated `

{pool.label}

` (`:37`, already the page `

`), the "{symbols} pool shares" line (`:38`, already the eyebrow), and the amber "No LP balance" pill (`:41-43`). | +| Dead dl row | `LpPositionPanel.tsx:92-95` | **Delete.** `
Underlying value
USD pricing unavailable
` is a string literal. It shows exclusively to users who *do* have a position. | +| Broken anchor | `LpPositionPanel.tsx:103` -> `${poolHref}#incentives` | `id="incentives"` only exists while the stake Modal is mounted (`IncentivesPanel.tsx:107`, `PoolDetailPage.tsx:63`). `LiquidityPage.tsx:32` renders the panel *without* `onStake`, so the broken link ships today. **Fix:** deep-link all three fallbacks as `?manage=add|remove|stake`, seed `manageAction` from `useSearchParams()` (validate against the union), clear the param on close. Update `LpPositionPanel.test.tsx:78-80`, which currently asserts the broken href and locks it in. | +| Meaningless stat | `PoolDetailPage.tsx:157-163` | `reserveCompositionPercent` normalizes each asset by its **own** decimals and then sums **across assets** (100 JUNO + 250 USDC = 350 -> "28.57% of token units"). That number moves with price, not with depth. **Delete** it, its call site (`:139`, `:144`), and the test assertion at `PoolDetailPage.test.tsx:115`. Retitle the section "Pool reserves". | +| Dual-direction price string + garbled hint | `PoolDetailPage.tsx:91`, `:165-173` | `formatCurrentPrice` prints both directions in one string (always tripping `metric-value-long` at `:132`), and the hint concatenates `poolType.swapCopy` with a fragment: *"Direct pair simulation returns contract pricing, spread, and fee for this XYK pool. Spot ratio from JUNO and USDC reserves"*. **Keep the price** (it is on-chain, survives an indexer outage; the chart readout is the last *indexed* close and renders only when `candles.data.length > 0`), but return one direction and replace the hint with a single sentence. `PoolDetailPage.test.tsx:116` still passes. | +| Chart is misnamed and half-dead | `PriceCandleChart.tsx:197-210`, `:20`, `theme.css:2345-2354` | `buildGeometry` produces a `candles` array (bodyY/bodyHeight/up) that is **never rendered**; only `linePath`/`areaPath` from `close` are drawn. Delete the `candles:` block, the unused `bodyWidth` (`:188`), and the `.candle-up`/`.candle-down` CSS. Fix `useState(compact ? "1h" : "1h")` -> `useState("1h")`. Do **not** rename the component (5 files, 8 test sites, zero user benefit). | +| Metrics section -> header strip | `PoolDetailPage.tsx:72-83` | Move TVL / 24h vol / APR into the header as a compact `.pool-header-stat` strip. **Carry the honesty affordances:** keep the `"Metrics unavailable"` fallback, keep `aprHint(metrics)` **visible** (not a `title` - invisible on touch and to keyboard), keep the `access.isStale` / `updatedAt` line. Delete the redundant `"TVL, 24h volume, and APR are unavailable"` sentence at `:79-81` (the three cards already say it three times). | +| Deposit form is invisible until you find a button | `PoolDetailPage.tsx:23, 61-63` | Optional Wave 3: `useState<"add"\|"remove"\|"stake">("add")` (drop `null`), replace the three ``s with a persistent tabbed action card (`role="tablist"`). **Keep the `Modal` component** - `TransactionReview` *is* a Modal (`TransactionReview.tsx:2`) and `TokenSelect` uses one. Un-nesting also fixes a real focus-trap leak: `Modal` does not portal, and only Escape is `stopPropagation`'d (`Modal.tsx:23-27`), so the outer Tab trap fires on bubble and can move focus from the confirm dialog into the form behind it. | +| Typography has no heading tier | `pools.css:248-256` | h3 (`:248`), metric labels (`:270`), and `dt` (`:336`) are all `0.75rem / mono / 700 / uppercase`. **Restyle** the h3 to `var(--font-display) 1.05rem 600, no transform` (do **not** delete the h3s - four sections use `aria-labelledby` against them). Then mono-uppercase consistently means "label/numeric" and nothing else. Fix the stale comment at `pools.css:348`; the `.liquidity-grid` rule itself is still live (Modal doesn't portal, so IncentivesPanel renders inside `.pool-detail-page`). | +| IncentivesPanel debug dump | `IncentivesPanel.tsx:115-121, 157` | Delete the "Incentives contract" (`:116`, already in the review `disclosures`), "Reward APR" (`:117`, already the pool APR), "Wallet LP" (`:118`, already in the TokenAmountInput balance), and "Pool reward rate" rows (`:120`, an undenominated `${rewardRps} reward units/sec`). Delete the hardcoded `"Protocol commission" -> "Unavailable from incentives query"` review row with `tone: "warning"` (`:157`) - it can never resolve and trains users to ignore warning tone. | +| AddLiquidityForm always-on advisory box | `AddLiquidityForm.tsx:248-251` | **Delete outright**, do not make it conditional. All three branches restate copy already on screen: the `!supportsProvideLiquidity` branch is byte-identical to the header at `:195`; the XYK branch restates copy the input labels carry at `:204`; the first-provider branch duplicates the warning box at `:235-245`. | +| Fake slippage control | `AddLiquidityForm.tsx:198` | An Interchain ` : null} + + ); +} + +export function OptionalDataState({ title, children, onRetry }: { title: string; children?: ReactNode; onRetry?: () => void }) { + return ( +
+ {title} + {children || onRetry ? ( +
+ More information + {children ?

{children}

: null} + {onRetry ? : null} +
+ ) : null} +
+ ); +} diff --git a/frontend/src/components/common/Toast.tsx b/frontend/src/components/common/Toast.tsx new file mode 100644 index 000000000..a96f0d1ee --- /dev/null +++ b/frontend/src/components/common/Toast.tsx @@ -0,0 +1,70 @@ +import type { ReactNode } from "react"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; + +type ToastKind = "pending" | "success" | "error"; +type Toast = { id: string; kind: ToastKind; title: string; message?: string; txHash?: ReactNode }; +type ToastInput = Omit; + +type ToastContextValue = { + pending: (toast: ToastInput) => string; + success: (toast: ToastInput) => string; + error: (toast: ToastInput) => string; + dismiss: (id: string) => void; +}; + +const ToastContext = createContext(undefined); + +function nextToastId() { + return `toast-${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + const push = useCallback((kind: ToastKind, toast: ToastInput) => { + const id = nextToastId(); + setToasts((current) => [...current, { ...toast, id, kind }]); + return id; + }, []); + const dismiss = useCallback((id: string) => setToasts((current) => current.filter((toast) => toast.id !== id)), []); + const value = useMemo(() => ({ + pending: (toast) => push("pending", toast), + success: (toast) => push("success", toast), + error: (toast) => push("error", toast), + dismiss, + }), [dismiss, push]); + + return ( + + {children} +
+ {toasts.map((toast) => dismiss(toast.id)} />)} +
+
+ ); +} + +function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { + const [paused, setPaused] = useState(false); + useEffect(() => { + if (toast.kind !== "success" || paused) return; + const timer = window.setTimeout(onDismiss, 6_000); + return () => window.clearTimeout(timer); + }, [onDismiss, paused, toast.kind]); + return ( +
setPaused(true)} onMouseLeave={() => setPaused(false)} onFocus={() => setPaused(true)} onBlur={(event) => { if (!event.currentTarget.contains(event.relatedTarget)) setPaused(false); }}> + +
+ {toast.title} + {toast.message ?

{toast.message}

: null} + {toast.txHash ?
{toast.txHash}
: null} +
+ +
+ ); +} + +export function useToast() { + const context = useContext(ToastContext); + if (!context) throw new Error("useToast must be used within ToastProvider"); + return context; +} diff --git a/frontend/src/components/common/TokenAmountInput.test.tsx b/frontend/src/components/common/TokenAmountInput.test.tsx new file mode 100644 index 000000000..469236a82 --- /dev/null +++ b/frontend/src/components/common/TokenAmountInput.test.tsx @@ -0,0 +1,79 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { TokenAmountInput } from "./TokenAmountInput"; + +describe("TokenAmountInput", () => { + it("emits decimal-safe base amounts", () => { + const onChange = vi.fn(); + render( + + ); + + fireEvent.change(screen.getByLabelText("From amount"), { + target: { value: "0.000001" }, + }); + + expect(onChange).toHaveBeenCalledWith("0.000001", "1"); + }); + + it("shows invalid precision errors", () => { + render( + + ); + + expect(screen.getByRole("alert").textContent).toContain( + "Too many decimal places" + ); + }); + + it("shows over-balance errors", () => { + render( + + ); + }); + + it("fires MAX and half callbacks with base amounts", () => { + const onChange = vi.fn(); + const onMax = vi.fn(); + const onHalf = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByRole("button", { name: /half/i })); + expect(onHalf).toHaveBeenCalledWith("617283"); + expect(onChange).toHaveBeenCalledWith("0.617283", "617283"); + + fireEvent.click(screen.getByRole("button", { name: /max/i })); + expect(onMax).toHaveBeenCalledWith("1234567"); + expect(onChange).toHaveBeenCalledWith("1.234567", "1234567"); + }); +}); diff --git a/frontend/src/components/common/TokenAmountInput.tsx b/frontend/src/components/common/TokenAmountInput.tsx new file mode 100644 index 000000000..6779b500a --- /dev/null +++ b/frontend/src/components/common/TokenAmountInput.tsx @@ -0,0 +1,133 @@ +import type { ReactNode } from "react"; +import { + formatAmount, + isBaseAmountGreaterThan, + parseTokenAmount, + toBaseAmount, +} from "../../lib/format/amounts"; + +export type TokenAmountInputProps = { + label: string; + value: string; + decimals: number; + symbol: string; + balanceBaseAmount?: string; + onChange: (value: string, baseAmount: string) => void; + onMax?: (baseAmount: string) => void; + onHalf?: (baseAmount: string) => void; + tokenSlot?: ReactNode; + fiatHint?: ReactNode; + disabled?: boolean; + showQuickActions?: boolean; + showTokenIdentity?: boolean; +}; + +function halfBaseAmount(balanceBaseAmount: string): string { + return (BigInt(balanceBaseAmount || "0") / 2n).toString(); +} + +export function TokenAmountInput({ + label, + value, + decimals, + symbol, + balanceBaseAmount, + onChange, + onMax, + onHalf, + tokenSlot, + fiatHint, + disabled, + showQuickActions = true, + showTokenIdentity = true, +}: TokenAmountInputProps) { + const parsed = parseTokenAmount(value, decimals); + const hasBalance = typeof balanceBaseAmount === "string"; + const isOverBalance = + hasBalance && + parsed.isValid && + isBaseAmountGreaterThan(parsed.baseAmount, balanceBaseAmount); + const error = parsed.error ?? (isOverBalance ? "Amount exceeds balance" : undefined); + const balanceCopy = hasBalance + ? `${formatAmount(balanceBaseAmount, decimals)} ${symbol}` + : "—"; + + const applyBaseAmount = ( + baseAmount: string, + callback?: (baseAmount: string) => void + ) => { + const displayValue = formatAmount(baseAmount, decimals, decimals).replace( + /,/g, + "" + ); + onChange(displayValue, toBaseAmount(displayValue, decimals)); + callback?.(baseAmount); + }; + + return ( +
+
+ {label} + + bal {balanceCopy} + +
+
+ {showTokenIdentity ? ( +
+ {tokenSlot ? ( + {tokenSlot} + ) : ( + {symbol.slice(0, 2)} + )} + {symbol} +
+ ) : null} + { + const nextValue = event.target.value; + onChange(nextValue, toBaseAmount(nextValue, decimals)); + }} + placeholder="0.0" + /> +
+ {showQuickActions || fiatHint ? ( +
+ {showQuickActions ? ( + <> + + + + ) : null} + {fiatHint ? {fiatHint} : null} +
+ ) : null} + {error ?

{error}

: null} +
+ ); +} diff --git a/frontend/src/components/common/TokenLogo.tsx b/frontend/src/components/common/TokenLogo.tsx new file mode 100644 index 000000000..b3350ea09 --- /dev/null +++ b/frontend/src/components/common/TokenLogo.tsx @@ -0,0 +1,22 @@ +import type { RegistryAsset } from "../../config/registry"; + +export function TokenLogo({ asset, size = "md" }: { asset: Pick; size?: "sm" | "md" }) { + const initials = asset.symbol.slice(0, 2).toUpperCase(); + if (asset.logoURI) { + return ( + + {`${asset.symbol} { + event.currentTarget.style.display = "none"; + event.currentTarget.parentElement?.setAttribute("data-fallback", initials); + }} + /> + + ); + } + return {initials}; +} diff --git a/frontend/src/components/common/TransactionReview.test.tsx b/frontend/src/components/common/TransactionReview.test.tsx new file mode 100644 index 000000000..ab8f57b8e --- /dev/null +++ b/frontend/src/components/common/TransactionReview.test.tsx @@ -0,0 +1,40 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { TransactionReview } from "./TransactionReview"; + +describe("TransactionReview", () => { + it("shows commitment, account, network, fee availability, and technical disclosure before confirmation", () => { + const confirm = vi.fn(); + render( + , + ); + + expect(screen.getByText("juno1wallet")).toBeTruthy(); + expect(screen.getByText("juno-1")).toBeTruthy(); + expect(screen.getByText(/≈ 0.00975 JUNO/i)).toBeTruthy(); + fireEvent.click(screen.getByText(/contracts and identifiers/i)); + expect(screen.getByText("juno1pair")).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: /confirm in wallet/i })); + expect(confirm).toHaveBeenCalledOnce(); + }); + + it("prevents confirmation when the reviewed snapshot is invalid", () => { + render(); + expect(screen.getByRole("button", { name: /confirm in wallet/i }).hasAttribute("disabled")).toBe(true); + }); +}); diff --git a/frontend/src/components/common/TransactionReview.tsx b/frontend/src/components/common/TransactionReview.tsx new file mode 100644 index 000000000..04a91a01b --- /dev/null +++ b/frontend/src/components/common/TransactionReview.tsx @@ -0,0 +1,80 @@ +import type { ReactNode } from "react"; +import { Modal } from "./Modal"; +import type { NetworkFeeEstimate } from "../../lib/cosmjs/fees"; + +export type TransactionReviewRow = { + label: string; + value: ReactNode; + tone?: "default" | "warning" | "danger"; +}; + +export type TransactionReviewDisclosure = { + label: string; + value: ReactNode; +}; + +export function TransactionReview({ + open, + title, + description, + account, + chainId, + rows, + disclosures = [], + networkFeeEstimate, + warning, + confirmDisabled = false, + pending = false, + onClose, + onConfirm, +}: { + open: boolean; + title: string; + description: ReactNode; + account?: string; + chainId?: string; + rows: TransactionReviewRow[]; + disclosures?: TransactionReviewDisclosure[]; + networkFeeEstimate?: NetworkFeeEstimate; + warning?: ReactNode; + confirmDisabled?: boolean; + pending?: boolean; + onClose: () => void; + onConfirm: () => void; +}) { + return ( + +
+

{description}

+
+
Connected account
{account ?? "Unavailable"}
+
Network
{chainId ?? "Unavailable"}
+ {rows.map((row) => ( +
+
{row.label}
+
{row.value}
+
+ ))} +
+
Estimated network fee
+
+ {networkFeeEstimate ? `≈ ${networkFeeEstimate.amountJuno} JUNO` : "Unavailable — wallet will calculate before signature"} +
+
+
+ {disclosures.length > 0 ? ( +
+ Contracts and identifiers +
+ {disclosures.map((item) =>
{item.label}
{item.value}
)} +
+
+ ) : null} + {warning ?
{warning}
: null} + +
+
+ ); +} diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts new file mode 100644 index 000000000..848afe5d3 --- /dev/null +++ b/frontend/src/components/common/index.ts @@ -0,0 +1,8 @@ +export { ExplorerLink } from "./ExplorerLink"; +export { Modal } from "./Modal"; +export { ToastProvider, useToast } from "./Toast"; +export { RiskBadge, RiskBadgeList, RiskAcknowledgement } from "./RiskBadges"; +export { EmptyState, ErrorState, OptionalDataState, Skeleton } from "./States"; +export { TokenAmountInput } from "./TokenAmountInput"; +export { TokenLogo } from "./TokenLogo"; +export { TransactionReview, type TransactionReviewDisclosure, type TransactionReviewRow } from "./TransactionReview"; diff --git a/frontend/src/components/create/CreatePoolPage.test.tsx b/frontend/src/components/create/CreatePoolPage.test.tsx new file mode 100644 index 000000000..94b5ee250 --- /dev/null +++ b/frontend/src/components/create/CreatePoolPage.test.tsx @@ -0,0 +1,65 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CreatePoolPage } from "./CreatePoolPage"; + +const mocks = vi.hoisted(() => ({ + mutate: vi.fn(), + configRefetch: vi.fn(), + duplicateRefetch: vi.fn(), + config: { + pair_configs: [ + { code_id: 1, pair_type: { xyk: {} }, total_fee_bps: 30, maker_fee_bps: 10, permissioned: false }, + ], + }, +})); + +vi.mock("@tanstack/react-query", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useQuery: (options: { queryKey: string[] }) => options.queryKey[0] === "factory-config" + ? { data: mocks.config, isLoading: false, isError: false, refetch: mocks.configRefetch } + : { data: null, isLoading: false, isFetching: false, isError: false, refetch: mocks.duplicateRefetch }, + }; +}); + +vi.mock("../../queries/useDexRegistry", () => ({ useDexRegistry: () => ({ pools: [] }) })); +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => ({ wallet: { status: "connected", address: "juno1wallet", signer: vi.fn() } }), + useNetworkGuard: () => ({ network: { expectedChainId: "juno-1", connectedChainId: "juno-1", isJunoReady: true, isWrongNetwork: false } }), +})); +vi.mock("../../mutations/useCreatePoolTx", () => ({ + buildCreatePoolExecuteInstruction: () => ({ contractAddress: "juno1factory", msg: {} }), + useCreatePoolTx: () => ({ mutate: mocks.mutate, isPending: false, isError: false, isSuccess: false, txState: { status: "idle", label: "Ready" } }), +})); + +describe("CreatePoolPage review", () => { + beforeEach(() => { + mocks.mutate.mockReset(); + mocks.configRefetch.mockReset(); + mocks.duplicateRefetch.mockReset(); + mocks.configRefetch.mockResolvedValue({ data: mocks.config, isError: false }); + mocks.duplicateRefetch.mockResolvedValue({ data: null, isError: false }); + }); + + it("rechecks factory state and reviews the empty-pool commitment before wallet confirmation", async () => { + render(); + + const reviewButton = await screen.findByRole("button", { name: /review pool creation/i }); + fireEvent.click(reviewButton); + + await waitFor(() => expect(mocks.configRefetch).toHaveBeenCalledOnce()); + expect(mocks.duplicateRefetch).toHaveBeenCalledOnce(); + expect(await screen.findByText(/creates an empty pool only/i)).toBeTruthy(); + expect(screen.getByText(/none — separate transaction required/i)).toBeTruthy(); + fireEvent.click(screen.getByText(/contracts and identifiers/i)); + expect(screen.getByText(/pool creation contract/i)).toBeTruthy(); + + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + expect(mocks.mutate).toHaveBeenCalledWith( + expect.objectContaining({ option: expect.objectContaining({ id: "xyk" }) }), + expect.objectContaining({ onSuccess: expect.any(Function) }), + ); + }); +}); diff --git a/frontend/src/components/create/CreatePoolPage.tsx b/frontend/src/components/create/CreatePoolPage.tsx new file mode 100644 index 000000000..5b12f2869 --- /dev/null +++ b/frontend/src/components/create/CreatePoolPage.tsx @@ -0,0 +1,224 @@ +import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { Box, Button, Stack, Text } from "@interchain-ui/react"; +import type { RegistryAsset } from "../../config/registry"; +import { dexRegistry } from "../../config/registry"; +import { toAssetInfo } from "../../lib/astroport/assetInfo"; +import { queryFactoryConfig, queryFactoryPair } from "../../lib/astroport/queries"; +import { buildCreatePoolAssets, createPoolOptions, makeCustomAsset, poolMatchesAssets, validateCreatePool, type CreatePoolConfigOption, type CreatePoolType } from "../../lib/createPool"; +import { buildCreatePoolExecuteInstruction, useCreatePoolTx } from "../../mutations/useCreatePoolTx"; +import { estimateExecuteNetworkFee, type NetworkFeeEstimate } from "../../lib/cosmjs/fees"; +import { useDexRegistry } from "../../queries/useDexRegistry"; +import { useNetworkGuard, useWallet } from "../../wallet/WalletContext"; +import { EmptyState, ErrorState, RiskAcknowledgement, Skeleton, TransactionReview } from "../common"; +import { TxStatusDialog } from "../tx/TxStatusDialog"; +import { TokenSelect } from "../swap/TokenSelect"; + +type AssetSide = "a" | "b"; +type CreatePoolReview = { + assets: [RegistryAsset, RegistryAsset]; + option: CreatePoolConfigOption; + configVersion: string; + networkFeeEstimate?: NetworkFeeEstimate; +}; + +function feeLabel(feeBps?: number) { + return typeof feeBps === "number" ? `${(feeBps / 100).toFixed(2)}% total fee` : "Factory default fee"; +} + +function inferCustomAssetKind(id: string): RegistryAsset["kind"] { + if (/^juno1[0-9a-z]+$/i.test(id)) return "cw20"; + if (/^ibc\//i.test(id)) return "ibc"; + return "native"; +} + +export function CreatePoolPage() { + const navigate = useNavigate(); + const { pools } = useDexRegistry(); + const { wallet } = useWallet(); + const { network } = useNetworkGuard(); + const [poolType, setPoolType] = useState("xyk"); + const [assetAId, setAssetAId] = useState("ujuno"); + const [assetBId, setAssetBId] = useState(""); + const [customAssets, setCustomAssets] = useState>>({}); + const [riskAcknowledged, setRiskAcknowledged] = useState(false); + const [review, setReview] = useState(); + const [isPreparingReview, setIsPreparingReview] = useState(false); + const [reviewError, setReviewError] = useState(); + const configQuery = useQuery({ queryKey: ["factory-config", dexRegistry.factory], queryFn: queryFactoryConfig, staleTime: 5 * 60_000, retry: 2 }); + const options = useMemo(() => createPoolOptions(configQuery.data?.pair_configs), [configQuery.data?.pair_configs]); + const selectedOption = options.find((option) => option.id === poolType) ?? options[0]; + const baseAssets = useMemo(() => buildCreatePoolAssets(pools), [pools]); + const selectableAssets = useMemo(() => { + const custom = [customAssets.a, customAssets.b].filter((asset): asset is RegistryAsset => Boolean(asset)); + const customIds = new Set(custom.map((asset) => asset.id)); + return [...custom, ...baseAssets.filter((asset) => !customIds.has(asset.id))]; + }, [baseAssets, customAssets]); + + useEffect(() => { + if (!assetBId) setAssetBId(selectableAssets.find((asset) => asset.id !== assetAId)?.id ?? ""); + }, [assetAId, assetBId, selectableAssets]); + + useEffect(() => setRiskAcknowledged(false), [assetAId, assetBId, poolType]); + + const assetA = selectableAssets.find((asset) => asset.id === assetAId); + const assetB = selectableAssets.find((asset) => asset.id === assetBId && asset.id !== assetAId); + const selectedAssets = assetA && assetB ? [assetA, assetB] as [RegistryAsset, RegistryAsset] : undefined; + const localDuplicate = selectedAssets ? pools.find((pool) => poolMatchesAssets(pool, selectedAssets)) : undefined; + const duplicateQuery = useQuery({ + queryKey: ["factory-pair", selectedAssets?.[0].id, selectedAssets?.[1].id], + enabled: Boolean(selectedAssets && !localDuplicate), + queryFn: async () => { + if (!selectedAssets) return null; + try { + return await queryFactoryPair([toAssetInfo(selectedAssets[0]), toAssetInfo(selectedAssets[1])]); + } catch (error) { + if (error instanceof Error && /404|not found|No pair|Pair was not found/i.test(error.message)) return null; + throw error; + } + }, + retry: 1, + staleTime: 30_000, + }); + const validation = validateCreatePool({ assets: [assetA, assetB], option: selectedOption, existingPair: localDuplicate ?? duplicateQuery.data, riskAcknowledged }); + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const signerOrClient = wallet.status === "connected" ? wallet.signer : undefined; + const createPoolTx = useCreatePoolTx(signerOrClient, walletAddress); + const submitDisabled = wallet.status !== "connected" || !network.isJunoReady || network.isWrongNetwork || configQuery.isError || duplicateQuery.isError || !validation.isValid || createPoolTx.isPending || isPreparingReview; + const actionCopy = network.isWrongNetwork + ? "Switch to Juno to create pool" + : wallet.status !== "connected" + ? "Connect wallet to create pool" + : createPoolTx.isPending + ? "Creating pool…" + : isPreparingReview ? "Rechecking availability…" : validation.error ?? "Review pool creation"; + + const handleCreateCustomAsset = (side: AssetSide, query: string) => { + const id = query.trim(); + if (!id) return; + const asset = makeCustomAsset({ kind: inferCustomAssetKind(id), id }); + setCustomAssets((current) => ({ ...current, [side]: asset })); + if (side === "a") setAssetAId(asset.id); + else setAssetBId(asset.id); + }; + + const prepareCreateReview = async () => { + if (submitDisabled || !selectedAssets || !selectedOption) return; + setIsPreparingReview(true); + setReviewError(undefined); + const [freshConfig, freshDuplicate] = await Promise.all([configQuery.refetch(), duplicateQuery.refetch()]); + setIsPreparingReview(false); + if (!freshConfig.data || freshConfig.isError) { + setReviewError("Current pool-creation settings could not be verified. Try again before reviewing."); + return; + } + if (localDuplicate || freshDuplicate.data) { + setReviewError("A pool for these assets now exists. Creation is blocked to avoid a duplicate market."); + return; + } + const freshOption = createPoolOptions(freshConfig.data.pair_configs).find((option) => option.id === selectedOption.id); + if (!freshOption || freshOption.disabled) { + setReviewError("The selected pool type is no longer available."); + return; + } + const reviewedAssets = [...selectedAssets] as [RegistryAsset, RegistryAsset]; + const instruction = buildCreatePoolExecuteInstruction({ assets: reviewedAssets, option: freshOption }); + const networkFeeEstimate = await estimateExecuteNetworkFee(signerOrClient, walletAddress, [instruction]).catch(() => undefined); + setReview({ assets: reviewedAssets, option: freshOption, configVersion: JSON.stringify(freshConfig.data.pair_configs), networkFeeEstimate }); + }; + + const reviewIsCurrent = Boolean(review + && review.assets[0].id === selectedAssets?.[0].id + && review.assets[1].id === selectedAssets?.[1].id + && review.option.id === selectedOption?.id + && review.configVersion === JSON.stringify(configQuery.data?.pair_configs)); + + const handleCreate = () => { + if (!review || !reviewIsCurrent || createPoolTx.isPending) return; + createPoolTx.mutate({ assets: review.assets, option: review.option }, { + onSuccess: (result) => { + setReview(undefined); + if (result.pairAddress) navigate(`/pools/${result.pairAddress}`); + }, + }); + }; + + return ( +
+

Create pool

+

Permissionless pool

+

Select two assets, choose an available pool type, and review the risks before asking your wallet to create the empty pool.

+ + + + + 1 · Assets + Select pair assets + + + + handleCreateCustomAsset("a", query)} /> + asset.id !== assetA?.id)} value={assetB?.id ?? ""} onChange={setAssetBId} label="Second asset" onCreateCustomAsset={(query) => handleCreateCustomAsset("b", query)} /> + + + + 2 · Pool type + {configQuery.isLoading ?
: null} + {configQuery.isError ? void configQuery.refetch()} /> : null} + {!configQuery.isLoading && options.length === 0 ? This network currently offers no pool type that can be created from the app. : null} +
+ {options.map((option) => ( + + ))} +
+
+ + {localDuplicate ?
Existing pool detected. Open {localDuplicate.label} instead of creating a duplicate.
: null} + {duplicateQuery.isFetching ?

Checking for an existing pool…

: null} +
Guardrails
    {validation.warnings.map((warning) =>
  • {warning}
  • )}
+ + {network.isWrongNetwork ? Transactions are blocked while your wallet is off Juno mainnet. : null} + {validation.error && wallet.status === "connected" && !network.isWrongNetwork ? {validation.error} : null} + {reviewError ?

{reviewError}

: null} + + + asset.verified !== true) ? "At least one asset is unverified. Verify its full identifier before signing." : undefined} + confirmDisabled={!reviewIsCurrent} + pending={createPoolTx.isPending} + onClose={() => setReview(undefined)} + onConfirm={handleCreate} + /> +
+
+ ); +} diff --git a/frontend/src/components/incentives/IncentivesPanel.test.tsx b/frontend/src/components/incentives/IncentivesPanel.test.tsx new file mode 100644 index 000000000..e261718f1 --- /dev/null +++ b/frontend/src/components/incentives/IncentivesPanel.test.tsx @@ -0,0 +1,144 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import type { IncentivesPoolState } from "../../lib/incentives"; +import { ToastProvider } from "../common"; +import { IncentivesPanel } from "./IncentivesPanel"; + +const pool: RegistryPool = { + id: "juno-token", + label: "JUNO / TOKEN", + pair: "juno1pair", + lpToken: "factory/juno1pair/astroport/share", + type: "xyk", + feeBps: 30, + enabled: true, + status: "active", + verified: true, + source: "registry", + explorer: "https://ping.pub/juno/wasm/contract/juno1pair", + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, verified: true }, + { kind: "native", id: "factory/pair/token", symbol: "TOKEN", decimals: 6, verified: true }, + ], +}; + +const mocks = vi.hoisted(() => ({ + wallet: { + status: "connected" as const, + address: "juno1wallet", + signer: vi.fn(), + }, + network: { + expectedChainId: "juno-1" as const, + connectedChainId: "juno-1", + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }, + incentives: { + data: { + configured: true, + contractAddress: "juno1incentives", + lpToken: "factory/juno1pair/astroport/share", + stakedAmount: "50000000", + pendingRewards: [{ info: { native_token: { denom: "ujuno" } }, amount: "1230000" }], + rewardInfo: [{ index: "0", orphaned: "0", rps: "0.25", reward: { ext: { info: { native_token: { denom: "factory/reward" } }, next_update_ts: 10 } } }], + } as IncentivesPoolState, + isLoading: false, + isError: false, + } as any, + mutateAsync: vi.fn(), + refetch: vi.fn(), +})); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => ({ wallet: mocks.wallet }), + useNetworkGuard: () => ({ network: mocks.network, switchToJuno: vi.fn() }), +})); + +vi.mock("../../queries/useWalletBalances", () => ({ + resolveDenom: () => ({ denom: pool.lpToken, symbol: "JUNO/TOKEN LP", decimals: 6, source: "lp" }), + getWalletBalanceAmount: () => "100000000", + useWalletBalances: () => ({ data: [{ denom: pool.lpToken, amount: "100000000" }], isError: false }), +})); + +vi.mock("../../queries/useIncentives", () => ({ + useIncentivesPool: () => mocks.incentives, +})); + +vi.mock("../../mutations/useIncentivesTx", () => ({ + buildIncentivesExecuteInstruction: () => ({ contractAddress: "juno1incentives", msg: {} }), + useIncentivesTx: () => ({ isPending: false, mutateAsync: mocks.mutateAsync, txState: { status: "idle", label: "Ready" } }), +})); + +function renderPanel() { + return render( + + + , + ); +} + +describe("IncentivesPanel", () => { + beforeEach(() => { + mocks.wallet.status = "connected"; + mocks.network.isWrongNetwork = false; + mocks.network.isJunoReady = true; + mocks.mutateAsync.mockReset(); + mocks.refetch.mockReset(); + mocks.incentives.data = { + configured: true, + contractAddress: "juno1incentives", + lpToken: pool.lpToken, + stakedAmount: "50000000", + pendingRewards: [{ info: { native_token: { denom: "ujuno" } }, amount: "1230000" }], + rewardInfo: [{ index: "0", orphaned: "0", rps: "0.25", reward: { ext: { info: { native_token: { denom: "factory/reward" } }, next_update_ts: 10 } } }], + }; + mocks.incentives.refetch = mocks.refetch; + mocks.refetch.mockImplementation(async () => ({ data: mocks.incentives.data })); + }); + + it("shows configured incentives, APR, staked LP, pending rewards, and external programs", () => { + renderPanel(); + + expect(screen.getByText("12.5% estimated")).toBeTruthy(); + expect(screen.getAllByText(/50 JUNO\/TOKEN LP/).length).toBeGreaterThanOrEqual(1); + expect(screen.getByText(/1.23 ujuno/)).toBeTruthy(); + expect(screen.getByText(/External factory\/reward/)).toBeTruthy(); + }); + + it("shows safe empty copy when incentives are unconfigured", () => { + mocks.incentives.data = { configured: false, lpToken: pool.lpToken, pendingRewards: [], rewardInfo: [] } as IncentivesPoolState; + renderPanel(); + + expect(screen.getByText(/No incentives contract is configured/i)).toBeTruthy(); + expect(screen.queryByRole("button", { name: "Stake LP" })).toBeNull(); + }); + + it("validates stake amount and submits the tx mutation payload", async () => { + mocks.mutateAsync.mockResolvedValueOnce({ transactionHash: "ABC" }); + renderPanel(); + + fireEvent.change(screen.getByLabelText("Stake LP amount"), { target: { value: "25" } }); + fireEvent.click(screen.getByRole("button", { name: "Review stake" })); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + await waitFor(() => expect(mocks.mutateAsync).toHaveBeenCalledWith({ action: "stake", pool, amount: "25000000" })); + }); + + it("validates unstake amount and submits claim rewards", async () => { + mocks.mutateAsync.mockResolvedValue({ transactionHash: "DEF" }); + renderPanel(); + + fireEvent.change(screen.getByLabelText("Unstake LP amount"), { target: { value: "10" } }); + fireEvent.click(screen.getByRole("button", { name: "Review unstake" })); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + fireEvent.click(screen.getByRole("button", { name: "Review claim" })); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + await waitFor(() => expect(mocks.mutateAsync).toHaveBeenCalledWith({ action: "unstake", pool, amount: "10000000" })); + await waitFor(() => expect(mocks.mutateAsync).toHaveBeenCalledWith({ action: "claim", pool, amount: undefined })); + }); +}); diff --git a/frontend/src/components/incentives/IncentivesPanel.tsx b/frontend/src/components/incentives/IncentivesPanel.tsx new file mode 100644 index 000000000..38b1ee6bd --- /dev/null +++ b/frontend/src/components/incentives/IncentivesPanel.tsx @@ -0,0 +1,241 @@ +import { useMemo, useState } from "react"; +import type { RegistryPool } from "../../config/registry"; +import type { Asset, AssetInfo, RewardInfo } from "../../lib/generated/Incentives.types"; +import { formatAmount, isBaseAmountGreaterThan, parseTokenAmount } from "../../lib/format/amounts"; +import { totalRewardRps, type IncentivesPoolState } from "../../lib/incentives"; +import type { PoolMetrics } from "../../lib/pools/poolList"; +import { buildIncentivesExecuteInstruction, useIncentivesTx } from "../../mutations/useIncentivesTx"; +import { estimateExecuteNetworkFee, type NetworkFeeEstimate } from "../../lib/cosmjs/fees"; +import { useIncentivesPool } from "../../queries/useIncentives"; +import { getWalletBalanceAmount, resolveDenom, useWalletBalances } from "../../queries/useWalletBalances"; +import { useNetworkGuard, useWallet } from "../../wallet/WalletContext"; +import { TokenAmountInput, TransactionReview } from "../common"; +import { TxStatusDialog } from "../tx/TxStatusDialog"; + +type IncentiveAction = "stake" | "unstake" | "claim"; +type IncentiveReview = { + action: IncentiveAction; + amount?: string; + stateVersion: string; + rewards: Asset[]; + networkFeeEstimate?: NetworkFeeEstimate; +}; + +function isPositiveBaseAmount(amount: string) { + return /^\d+$/.test(amount) && BigInt(amount) > 0n; +} + +export function IncentivesPanel({ pool, metrics }: { pool: RegistryPool; metrics?: PoolMetrics }) { + const { wallet } = useWallet(); + const { network } = useNetworkGuard(); + const [stakeAmount, setStakeAmount] = useState(""); + const [unstakeAmount, setUnstakeAmount] = useState(""); + const [review, setReview] = useState(); + const [isPreparingReview, setIsPreparingReview] = useState(false); + const [reviewError, setReviewError] = useState(); + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const balances = useWalletBalances(walletAddress, [pool]); + const incentives = useIncentivesPool(pool, walletAddress); + const lp = resolveDenom(pool.lpToken, [pool]); + const lpBalance = getWalletBalanceAmount(balances.data, pool.lpToken); + const stakedBalance = incentives.data?.stakedAmount; + const parsedStake = parseTokenAmount(stakeAmount, lp.decimals); + const parsedUnstake = parseTokenAmount(unstakeAmount, lp.decimals); + const stakeBaseAmount = parsedStake.baseAmount; + const unstakeBaseAmount = parsedUnstake.baseAmount; + const stakeExceedsBalance = Boolean(lpBalance && parsedStake.isValid && isBaseAmountGreaterThan(stakeBaseAmount, lpBalance)); + const unstakeExceedsBalance = Boolean(stakedBalance && parsedUnstake.isValid && isBaseAmountGreaterThan(unstakeBaseAmount, stakedBalance)); + const hasPendingRewards = (incentives.data?.pendingRewards ?? []).some((reward) => isPositiveBaseAmount(reward.amount)); + const signerOrClient = wallet.status === "connected" ? wallet.signer : undefined; + const incentivesTx = useIncentivesTx(signerOrClient, walletAddress); + const rewardRps = useMemo(() => totalRewardRps(incentives.data?.rewardInfo ?? []), [incentives.data?.rewardInfo]); + + const configured = incentives.data?.configured ?? false; + const canUseWallet = wallet.status === "connected" && network.isJunoReady && !network.isWrongNetwork && configured && !incentivesTx.isPending && !isPreparingReview; + const canStake = canUseWallet && parsedStake.isValid && isPositiveBaseAmount(stakeBaseAmount) && !stakeExceedsBalance; + const canUnstake = canUseWallet && parsedUnstake.isValid && isPositiveBaseAmount(unstakeBaseAmount) && !unstakeExceedsBalance; + const canClaim = canUseWallet && hasPendingRewards; + + const incentivesStateVersion = (state: IncentivesPoolState | undefined) => JSON.stringify({ + contractAddress: state?.contractAddress, + stakedAmount: state?.stakedAmount, + rewards: state?.pendingRewards?.map((reward) => [assetInfoLabel(reward.info), reward.amount]), + }); + + const prepareReview = async (action: IncentiveAction) => { + if ((action === "stake" && !canStake) || (action === "unstake" && !canUnstake) || (action === "claim" && !canClaim)) return; + setIsPreparingReview(true); + setReviewError(undefined); + const refreshed = await incentives.refetch(); + setIsPreparingReview(false); + if (!refreshed.data?.configured || !refreshed.data.contractAddress) { + setReviewError("Fresh incentives state could not be verified. Review remains unavailable."); + return; + } + const amount = action === "stake" ? stakeBaseAmount : action === "unstake" ? unstakeBaseAmount : undefined; + if (action === "unstake" && (!refreshed.data.stakedAmount || BigInt(amount ?? "0") > BigInt(refreshed.data.stakedAmount))) { + setReviewError("The refreshed staked balance is lower than this unstake amount."); + return; + } + const freshRewards = refreshed.data.pendingRewards.filter((reward) => isPositiveBaseAmount(reward.amount)); + if (action === "claim" && freshRewards.length === 0) { + setReviewError("No claimable rewards remain after refresh."); + return; + } + const instruction = buildIncentivesExecuteInstruction({ action, pool, amount }); + const networkFeeEstimate = await estimateExecuteNetworkFee(signerOrClient, walletAddress, [instruction]).catch(() => undefined); + setReview({ action, amount, stateVersion: incentivesStateVersion(refreshed.data), rewards: freshRewards, networkFeeEstimate }); + }; + + const reviewIsCurrent = Boolean(review + && review.stateVersion === incentivesStateVersion(incentives.data) + && (review.action !== "stake" || review.amount === stakeBaseAmount) + && (review.action !== "unstake" || review.amount === unstakeBaseAmount)); + + const submit = async () => { + if (!review || !reviewIsCurrent) return; + const { action, amount } = review; + try { + await incentivesTx.mutateAsync({ action, pool, amount }); + if (action === "stake") setStakeAmount(""); + if (action === "unstake") setUnstakeAmount(""); + setReview(undefined); + } catch { /* Shared transaction runner owns failure state and recovery copy. */ } + }; + + return ( +
+

Incentives

+

Stake LP shares in the configured incentives contract to accrue internal and external pool rewards. Rewards and APR appear when reward data is available.

+ + {!configured ? ( +

No incentives contract is configured for this deployment. LP staking, reward APR, and claiming are hidden rather than estimated.

+ ) : ( + <> +
+
Incentives contract
{incentives.data?.contractAddress}
+
Reward APR
{typeof metrics?.incentivesApr === "number" ? `${formatPercent(metrics.incentivesApr)} estimated` : "Unavailable"}
+
Wallet LP
{lpBalance ? `${formatAmount(lpBalance, lp.decimals)} ${lp.symbol}` : wallet.status === "connected" ? "Loading…" : "Connect wallet"}
+
Staked LP
{stakedBalance ? `${formatAmount(stakedBalance, lp.decimals)} ${lp.symbol}` : wallet.status === "connected" ? "0 or unavailable" : "Connect wallet"}
+
Pool reward rate
{rewardRps ? `${rewardRps} reward units/sec` : "No active reward rate reported"}
+
+ {incentives.data?.queryError ?

Some incentive balances are temporarily unavailable. You can retry by reopening this panel.

: null} + + + +
+
+ + {stakeExceedsBalance ?

Stake amount exceeds wallet LP balance.

: null} + +
+
+ + {unstakeExceedsBalance ?

Unstake amount exceeds staked LP balance.

: null} + +
+
+ + {reviewError ?

{reviewError}

: null} + + {network.isWrongNetwork ?

Switch to Juno to use incentives.

: null} + {wallet.status !== "connected" ?

Connect a wallet to load staked LP balances, pending rewards, and claim actions.

: null} + + )} + ({ label: `${assetInfoLabel(reward.info)} reward · estimated`, value: formatAmount(reward.amount, 6) })) : []), + { label: "Reward APR", value: typeof metrics?.incentivesApr === "number" ? `${formatPercent(metrics.incentivesApr)} · estimated` : "Unavailable", tone: typeof metrics?.incentivesApr === "number" ? "default" as const : "warning" as const }, + { label: "Pool status", value: `${pool.status}${pool.verified === true ? ", verified" : ", unverified"}` }, + ] : []} + disclosures={[ + { label: "Incentives contract", value: incentives.data?.contractAddress ?? "Unavailable" }, + { label: "LP token", value: pool.lpToken }, + { label: "Pair contract", value: pool.pair }, + ]} + warning={!reviewIsCurrent && review ? "The action amount or incentives state changed. Close this review and prepare a new one." : "Reward and fee data can change and some fee data is unavailable. Verify the wallet message before signing."} + confirmDisabled={!reviewIsCurrent} + pending={incentivesTx.isPending} + onClose={() => setReview(undefined)} + onConfirm={() => void submit()} + /> +
+ ); +} + +function RewardList({ rewards, title, empty }: { rewards: Asset[]; title: string; empty: string }) { + const nonZero = rewards.filter((reward) => isPositiveBaseAmount(reward.amount)); + return ( +
+

{title}

+ {nonZero.length === 0 ?

{empty}

: ( +
    + {nonZero.map((reward) =>
  • {formatAmount(reward.amount, 6)} {assetInfoLabel(reward.info)}
  • )} +
+ )} +
+ ); +} + +function RewardInfoList({ rewards }: { rewards: RewardInfo[] }) { + if (rewards.length === 0) return

No internal or external incentive programs are reported for this pool.

; + return ( +
+

Reward programs

+
    + {rewards.map((reward, index) =>
  • {rewardTypeLabel(reward)} · rps {reward.rps}
  • )} +
+
+ ); +} + +function rewardTypeLabel(reward: RewardInfo) { + if ("int" in reward.reward) return `Internal ${assetInfoLabel(reward.reward.int)}`; + return `External ${assetInfoLabel(reward.reward.ext.info)}`; +} + +function assetInfoLabel(info: AssetInfo) { + if ("native_token" in info) return info.native_token.denom; + return info.token.contract_addr; +} + +function formatPercent(value: number) { + return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value)}%`; +} + +function actionTitle(action: "stake" | "unstake" | "claim") { + if (action === "stake") return "Stake LP"; + if (action === "unstake") return "Unstake LP"; + return "Claim rewards"; +} + +function stakeButtonCopy(walletStatus: string, wrongNetwork: boolean, valid: boolean, amount: string, exceeds: boolean, pending: boolean) { + if (wrongNetwork) return "Switch to Juno to stake"; + if (walletStatus !== "connected") return "Connect wallet to stake"; + if (!valid || !isPositiveBaseAmount(amount)) return "Enter LP amount to stake"; + if (exceeds) return "Insufficient LP balance"; + return pending ? "Staking…" : "Stake LP"; +} + +function unstakeButtonCopy(walletStatus: string, wrongNetwork: boolean, valid: boolean, amount: string, exceeds: boolean, pending: boolean) { + if (wrongNetwork) return "Switch to Juno to unstake"; + if (walletStatus !== "connected") return "Connect wallet to unstake"; + if (!valid || !isPositiveBaseAmount(amount)) return "Enter LP amount to unstake"; + if (exceeds) return "Insufficient staked LP"; + return pending ? "Unstaking…" : "Unstake LP"; +} + +function claimButtonCopy(walletStatus: string, wrongNetwork: boolean, hasPendingRewards: boolean, pending: boolean) { + if (wrongNetwork) return "Switch to Juno to claim"; + if (walletStatus !== "connected") return "Connect wallet to claim"; + if (!hasPendingRewards) return "No rewards to claim"; + return pending ? "Claiming…" : "Claim rewards"; +} diff --git a/frontend/src/components/layout/DexShell.tsx b/frontend/src/components/layout/DexShell.tsx new file mode 100644 index 000000000..aed5fe8d6 --- /dev/null +++ b/frontend/src/components/layout/DexShell.tsx @@ -0,0 +1,138 @@ +import { useEffect, useRef, useState, type ReactNode } from "react"; +import { NavLink, useLocation } from "react-router-dom"; +import junoLogo from "../../assets/juno-logo-salmon.svg"; +import junoWordmark from "../../assets/juno-wordmark-salmon.svg"; +import { navigationItems, walletNavigationItems } from "../../app/routes"; +import { WalletProvider } from "../../wallet/WalletContext"; +import { NetworkGuardBanner } from "../wallet/NetworkGuardBanner"; +import { WalletConnectButton } from "../wallet/WalletConnectButton"; +import { ChainStatusBadge } from "../wallet/ChainStatusBadge"; +import { IndexerStatusBadge } from "../wallet/IndexerStatusBadge"; +import { junoDeployment } from "../../config/deployment"; +import { SlippageSettingsProvider } from "../../settings/SlippageSettingsContext"; +import { navIconByRoute } from "./NavIcons"; +import { useWallet } from "../../wallet/WalletContext"; +import { useTxHistory } from "../../tx/TxHistoryContext"; + +export function DexShell({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + +function DexShellContent({ children }: { children: ReactNode }) { + const [isNavOpen, setIsNavOpen] = useState(false); + const isMobileLayout = useMediaQuery("(max-width: 860px)"); + const { wallet, network } = useWallet(); + const location = useLocation(); + const mainRef = useRef(null); + useEffect(() => setIsNavOpen(false), [location.pathname]); + const currentRoute = navigationItems.find((item) => location.pathname === item.to || location.pathname.startsWith(`${item.to}/`)); + const pageTitle = currentRoute?.label ?? "Swap"; + const coordByPrefix: Array<[string, string]> = [ + ["/swap", "Swap"], + ["/pools", "Pools"], + ["/portfolio", "Portfolio"], + ["/create", "Create pool"], + ]; + const topbarCoord = coordByPrefix.find(([prefix]) => location.pathname === prefix || location.pathname.startsWith(`${prefix}/`))?.[1] ?? pageTitle; + useEffect(() => { + document.title = `${topbarCoord} | JUNO DEX`; + mainRef.current?.focus(); + }, [location.pathname, topbarCoord]); + + const visibleNavigationItems = wallet.status === "connected" ? [...navigationItems, ...walletNavigationItems] : navigationItems; + + return ( +
+ mainRef.current?.focus()}>Skip to main content +

{topbarCoord} page loaded

+
+
+ + + +

+ Juno + DEX +

+
+
+ {isMobileLayout ?
: null} + +
+ + +
+ + +
+
+ +
+ {topbarCoord} +
+ {!isMobileLayout ? : null} +
+
+ + + +
{children}
+ {isMobileLayout ? : null} +
+ ); +} + +function useMediaQuery(query: string) { + const [matches, setMatches] = useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false); + useEffect(() => { + if (typeof window.matchMedia !== "function") return; + const media = window.matchMedia(query); + const update = () => setMatches(media.matches); + update(); + media.addEventListener("change", update); + return () => media.removeEventListener("change", update); + }, [query]); + return matches; +} + +function MobileQuickNav({ walletConnected }: { walletConnected: boolean }) { + const { records, setCenterOpen } = useTxHistory(); + return ( + + ); +} diff --git a/frontend/src/components/layout/NavIcons.tsx b/frontend/src/components/layout/NavIcons.tsx new file mode 100644 index 000000000..81a923f38 --- /dev/null +++ b/frontend/src/components/layout/NavIcons.tsx @@ -0,0 +1,80 @@ +/* Single-stroke, currentColor icons in the Lucide idiom used by the Juno + design system. Kept local as inline SVG so the shell has no icon-font or + third-party dependency. 1.5px stroke, 18px default box. */ +import type { ReactElement, ReactNode, SVGProps } from "react"; + +type IconProps = SVGProps & { size?: number }; + +function Base({ size = 18, children, ...props }: IconProps & { children: ReactNode }) { + return ( + + ); +} + +export function SwapIcon(props: IconProps) { + return ( + + + + + ); +} + +export function PoolsIcon(props: IconProps) { + return ( + + + + + ); +} + +export function StatsIcon(props: IconProps) { + return ( + + + + + + ); +} + +export function PortfolioIcon(props: IconProps) { + return ( + + + + + ); +} + +export function CreateIcon(props: IconProps) { + return ( + + + + + ); +} + +export const navIconByRoute: Record ReactElement> = { + "/swap": SwapIcon, + "/pools": PoolsIcon, + "/stats": StatsIcon, + "/portfolio": PortfolioIcon, + "/create": CreateIcon, +}; diff --git a/frontend/src/components/liquidity/AddLiquidityForm.test.tsx b/frontend/src/components/liquidity/AddLiquidityForm.test.tsx new file mode 100644 index 000000000..33ca685d0 --- /dev/null +++ b/frontend/src/components/liquidity/AddLiquidityForm.test.tsx @@ -0,0 +1,180 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { AddLiquidityForm } from "./AddLiquidityForm"; + +const mocks = vi.hoisted(() => ({ + wallet: { + wallet: { status: "connected", address: "juno1wallet", signer: vi.fn() } as { status: "idle" | "connected"; address?: string; signer?: unknown }, + connect: vi.fn(), + }, + network: { + network: { + expectedChainId: "juno-1" as const, + connectedChainId: "juno-1", + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }, + switchToJuno: vi.fn(), + }, + balances: [ + { denom: "ujuno", amount: "1000000" }, + { denom: "ibc/test", amount: "2000000" }, + ], + poolData: { + assets: [{ amount: "1000000" }, { amount: "2000000" }], + total_share: "1000000", + }, + mutate: vi.fn(), + refetch: vi.fn(), +})); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => mocks.wallet, + useNetworkGuard: () => mocks.network, +})); + +vi.mock("../../queries/useWalletBalances", () => ({ + useWalletBalances: () => ({ data: mocks.balances }), + getWalletBalanceAmount: (balances: typeof mocks.balances, denom: string) => balances.find((balance) => balance.denom === denom)?.amount, +})); + +vi.mock("../../queries/usePools", () => ({ + usePoolReserves: () => ({ data: mocks.poolData, refetch: mocks.refetch }), +})); + +vi.mock("../../settings/SlippageSettingsContext", () => ({ + useSlippageSettings: () => ({ slippageBps: 50, formattedSlippagePercent: "0.5", maxSpread: "0.005" }), +})); + +vi.mock("../../mutations/useProvideLiquidityTx", () => ({ + buildProvideLiquidityExecuteInstruction: () => ({ contractAddress: "juno1pair", msg: {} }), + useProvideLiquidityTx: () => ({ mutate: mocks.mutate, isPending: false, isError: false, isSuccess: false, txState: { status: "idle", label: "Ready" } }), +})); + +const pool: RegistryPool = { + id: "test", + label: "JUNO / TEST", + pair: "juno1pair", + lpToken: "factory/juno1pair/lp", + type: "xyk", + feeBps: 30, + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, verified: true }, + { kind: "ibc", id: "ibc/test", symbol: "TEST", decimals: 6, verified: true }, + ], + explorer: "https://ping.pub/juno/address/juno1pair", + enabled: true, + status: "active", + verified: true, + source: "registry", +}; + +describe("AddLiquidityForm", () => { + beforeEach(() => { + mocks.mutate.mockReset(); + mocks.refetch.mockReset(); + mocks.poolData = { + assets: [{ amount: "1000000" }, { amount: "2000000" }], + total_share: "1000000", + }; + mocks.wallet.wallet = { status: "connected", address: "juno1wallet", signer: vi.fn() }; + mocks.network.network = { + expectedChainId: "juno-1", + connectedChainId: "juno-1", + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }; + mocks.refetch.mockImplementation(async () => ({ data: mocks.poolData })); + }); + + it("auto-balances, reviews fresh reserves, and submits proportional add liquidity", async () => { + const { container } = render(); + + fireEvent.change(screen.getByLabelText("JUNO amount · driving ratio amount"), { target: { value: "0.1" } }); + + expect((screen.getByLabelText("TEST amount · auto-balanced amount") as HTMLInputElement).value).toBe("0.2"); + expect(container.textContent).toContain("Expected LP tokens: 0.1"); + + fireEvent.click(screen.getByRole("button", { name: /^review add liquidity$/i })); + expect(mocks.refetch).toHaveBeenCalledOnce(); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + expect(mocks.mutate).toHaveBeenCalledWith({ + pool, + amounts: ["100000", "200000"], + slippageTolerance: "0.005", + minLpToReceive: "99500", + }); + }); + + it("blocks submission on the wrong network", () => { + mocks.network.network = { ...mocks.network.network, connectedChainId: "osmosis-1", isWrongNetwork: true, isJunoReady: false }; + + render(); + + expect(screen.getByRole("button", { name: /switch to juno to add liquidity/i }).hasAttribute("disabled")).toBe(true); + }); + + it("disables stable and PCL add liquidity until type-specific provide math is wired", () => { + render(); + + expect(screen.getAllByText(/PCL provide rules depend on concentration parameters/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getByRole("button", { name: /PCL add liquidity is not supported in the UI yet/i }).hasAttribute("disabled")).toBe(true); + }); + + it("disables CW20 liquidity deposits until exact allowances are implemented", () => { + const cw20Pool: RegistryPool = { + ...pool, + assets: [pool.assets[0], { kind: "cw20", id: "juno1cw20token000000000000000000000000000000000", symbol: "CW20", decimals: 6, verified: true }], + }; + render(); + expect(screen.getByRole("button", { name: /cw20 add liquidity is unavailable/i }).hasAttribute("disabled")).toBe(true); + }); + + it("detects an empty XYK pool and shows first-provider guardrails", () => { + mocks.poolData = { assets: [{ amount: "0" }, { amount: "0" }], total_share: "0" }; + + render(); + + expect(screen.getByText("Seed initial liquidity")).toBeTruthy(); + expect(screen.getByText("First-provider warning")).toBeTruthy(); + expect(screen.getByPlaceholderText("SEED")).toBeTruthy(); + }); + + it("requires typed acknowledgement and review before first deposit", async () => { + mocks.poolData = { assets: [{ amount: "0" }, { amount: "0" }], total_share: "0" }; + render(); + + fireEvent.change(screen.getByLabelText("JUNO initial amount amount"), { target: { value: "0.1" } }); + fireEvent.change(screen.getByLabelText("TEST initial amount amount"), { target: { value: "0.5" } }); + + expect(screen.getByText(/1 JUNO = 5 TEST/i)).toBeTruthy(); + expect(screen.getByRole("button", { name: /type seed to acknowledge/i }).hasAttribute("disabled")).toBe(true); + + fireEvent.change(screen.getByPlaceholderText("SEED"), { target: { value: "SEED" } }); + fireEvent.click(screen.getByRole("button", { name: /^review initial liquidity$/i })); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + expect(mocks.mutate).toHaveBeenCalledWith({ + pool, + amounts: ["100000", "500000"], + slippageTolerance: "0.005", + minLpToReceive: undefined, + }); + }); + + it("keeps proportional add-liquidity behavior for non-empty pools", () => { + render(); + + expect(screen.queryByText("First-provider warning")).toBeNull(); + fireEvent.change(screen.getByLabelText("JUNO amount · driving ratio amount"), { target: { value: "0.1" } }); + + expect((screen.getByLabelText("TEST amount · auto-balanced amount") as HTMLInputElement).value).toBe("0.2"); + expect(screen.getByRole("button", { name: /^review add liquidity$/i }).hasAttribute("disabled")).toBe(false); + }); +}); diff --git a/frontend/src/components/liquidity/AddLiquidityForm.tsx b/frontend/src/components/liquidity/AddLiquidityForm.tsx new file mode 100644 index 000000000..4505d5278 --- /dev/null +++ b/frontend/src/components/liquidity/AddLiquidityForm.tsx @@ -0,0 +1,285 @@ +import { useMemo, useState } from "react"; +import { Box, Button, Stack, Text } from "@interchain-ui/react"; +import type { RegistryPool } from "../../config/registry"; +import { displayBaseAmount, calculateInitialLiquidityQuote, calculateProvideLiquidityQuote, formatLpShareBps, ratioAmount } from "../../lib/liquidity/provide"; +import { formatAmount, isBaseAmountGreaterThan, parseTokenAmount, toBaseAmount } from "../../lib/format/amounts"; +import { getPoolTypeMetadata } from "../../lib/pools/poolTypes"; +import { assessPoolRisk } from "../../lib/risk"; +import { slippageBpsToMaxSpread } from "../../lib/swap/slippage"; +import { buildProvideLiquidityExecuteInstruction, useProvideLiquidityTx } from "../../mutations/useProvideLiquidityTx"; +import { estimateExecuteNetworkFee, type NetworkFeeEstimate } from "../../lib/cosmjs/fees"; +import { usePoolReserves } from "../../queries/usePools"; +import { getWalletBalanceAmount, useWalletBalances } from "../../queries/useWalletBalances"; +import { useSlippageSettings } from "../../settings/SlippageSettingsContext"; +import { useNetworkGuard, useWallet } from "../../wallet/WalletContext"; +import { RiskAcknowledgement, RiskBadgeList, TokenAmountInput, TransactionReview } from "../common"; +import { TxStatusDialog } from "../tx/TxStatusDialog"; + +type AddLiquidityReview = { + amounts: [string, string]; + minLpToReceive?: string; + expectedLpAmount?: string; + poolShare?: string; + slippageBps: number; + reserveVersion: string; + isFirstProvider: boolean; + networkFeeEstimate?: NetworkFeeEstimate; +}; + +function hasPositiveBaseAmount(amount: string): boolean { + return /^\d+$/.test(amount) && BigInt(amount) > 0n; +} + +function applySlippageFloor(amount: string, slippageBps: number): string { + if (!/^\d+$/.test(amount)) return "0"; + return ((BigInt(amount) * BigInt(10_000 - slippageBps)) / 10_000n).toString(); +} + +export function AddLiquidityForm({ pool }: { pool: RegistryPool }) { + const { wallet, connect } = useWallet(); + const { network, switchToJuno } = useNetworkGuard(); + const { slippageBps, formattedSlippagePercent, maxSpread } = useSlippageSettings(); + const [amounts, setAmounts] = useState<[string, string]>(["", ""]); + const [lastEditedIndex, setLastEditedIndex] = useState<0 | 1>(0); + const [riskAcknowledged, setRiskAcknowledged] = useState(false); + const [seedAcknowledgement, setSeedAcknowledgement] = useState(""); + const [review, setReview] = useState(); + const [isPreparingReview, setIsPreparingReview] = useState(false); + const [reviewError, setReviewError] = useState(); + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const balances = useWalletBalances(walletAddress, [pool]); + const reserves = usePoolReserves(pool); + const signerOrClient = wallet.status === "connected" ? wallet.signer : undefined; + const provideTx = useProvideLiquidityTx(signerOrClient, walletAddress); + const poolType = getPoolTypeMetadata(pool.type); + + const reserveAmounts = useMemo<[string, string] | undefined>(() => { + const poolAssets = reserves.data?.assets; + if (!poolAssets || poolAssets.length < 2) return undefined; + return [poolAssets[0]?.amount ?? "0", poolAssets[1]?.amount ?? "0"]; + }, [reserves.data?.assets]); + + const baseAmounts = useMemo<[string, string]>(() => [ + toBaseAmount(amounts[0], pool.assets[0].decimals), + toBaseAmount(amounts[1], pool.assets[1].decimals), + ], [amounts, pool.assets]); + + const initialQuote = calculateInitialLiquidityQuote({ + depositAmounts: baseAmounts, + decimals: [pool.assets[0].decimals, pool.assets[1].decimals], + reserves: reserveAmounts, + totalShare: reserves.data?.total_share, + }); + const isFirstProvider = poolType.supportsProvideLiquidity && initialQuote.isFirstProvider; + const quote = reserveAmounts && !isFirstProvider + ? calculateProvideLiquidityQuote({ depositAmounts: baseAmounts, reserves: reserveAmounts, totalShare: reserves.data?.total_share ?? "0" }) + : null; + const risk = assessPoolRisk(pool, reserves.data); + const minLpToReceive = quote ? applySlippageFloor(quote.expectedLpAmount, slippageBps) : undefined; + + const updateAmount = (index: 0 | 1, nextAmount: string) => { + setLastEditedIndex(index); + const nextBase = toBaseAmount(nextAmount, pool.assets[index].decimals); + setAmounts((current) => { + const updated: [string, string] = [...current] as [string, string]; + updated[index] = nextAmount; + const otherIndex = index === 0 ? 1 : 0; + if (reserveAmounts && !isFirstProvider && hasPositiveBaseAmount(nextBase)) { + const otherBase = ratioAmount(nextBase, reserveAmounts[index], reserveAmounts[otherIndex]); + updated[otherIndex] = otherBase === "0" ? "" : displayBaseAmount(otherBase, pool.assets[otherIndex].decimals); + } else if (!hasPositiveBaseAmount(nextBase)) { + updated[otherIndex] = ""; + } + return updated; + }); + }; + + const validationError = useMemo(() => { + const parsed0 = parseTokenAmount(amounts[0], pool.assets[0].decimals); + const parsed1 = parseTokenAmount(amounts[1], pool.assets[1].decimals); + if (risk.blocked) return "This pool or one of its assets is blocked"; + if (!poolType.supportsProvideLiquidity) return `${poolType.shortLabel} add liquidity is not supported in the UI yet`; + if (pool.assets.some((asset) => asset.kind === "cw20")) return "CW20 add liquidity is unavailable until exact token allowances are implemented"; + if (!parsed0.isValid) return `${pool.assets[0].symbol}: ${parsed0.error}`; + if (!parsed1.isValid) return `${pool.assets[1].symbol}: ${parsed1.error}`; + if (!hasPositiveBaseAmount(baseAmounts[0]) || !hasPositiveBaseAmount(baseAmounts[1])) return "Enter both token amounts"; + const balance0 = getWalletBalanceAmount(balances.data, pool.assets[0].id); + const balance1 = getWalletBalanceAmount(balances.data, pool.assets[1].id); + if (balance0 && isBaseAmountGreaterThan(baseAmounts[0], balance0)) return `${pool.assets[0].symbol} amount exceeds wallet balance`; + if (balance1 && isBaseAmountGreaterThan(baseAmounts[1], balance1)) return `${pool.assets[1].symbol} amount exceeds wallet balance`; + if (!reserveAmounts) return "Pool reserves are still loading"; + if (isFirstProvider && seedAcknowledgement.trim() !== "SEED") return "Type SEED to acknowledge first-provider price setting"; + if (!isFirstProvider) { + if (!quote) return "Pool share estimate unavailable"; + if (!quote.isProportional) return "Amounts must match the current pool ratio"; + } + if (risk.requiresAcknowledgement && !riskAcknowledged) return "Acknowledge unverified pool"; + return undefined; + }, [amounts, balances.data, baseAmounts, isFirstProvider, pool.assets, poolType.shortLabel, poolType.supportsProvideLiquidity, quote, reserveAmounts, risk.blocked, risk.requiresAcknowledgement, riskAcknowledged, seedAcknowledgement]); + + const submitDisabled = Boolean(validationError) + || wallet.status !== "connected" + || network.isWrongNetwork + || provideTx.isPending + || isPreparingReview; + const actionCopy = network.isWrongNetwork + ? "Switch to Juno to add liquidity" + : wallet.status !== "connected" + ? "Connect wallet to add liquidity" + : validationError ?? (provideTx.isPending ? "Broadcasting…" : isPreparingReview ? "Refreshing reserves…" : isFirstProvider ? "Review initial liquidity" : "Review add liquidity"); + + const onSubmit = async () => { + if (wallet.status !== "connected") { + await connect(); + return; + } + if (network.isWrongNetwork) { + await switchToJuno(); + return; + } + if (submitDisabled) return; + setIsPreparingReview(true); + setReviewError(undefined); + const refreshed = await reserves.refetch(); + setIsPreparingReview(false); + const freshAssets = refreshed.data?.assets; + if (!freshAssets || freshAssets.length < 2) { + setReviewError("Fresh pool reserves could not be loaded. Review remains unavailable."); + return; + } + const freshReserveAmounts: [string, string] = [freshAssets[0]?.amount ?? "0", freshAssets[1]?.amount ?? "0"]; + const freshInitial = calculateInitialLiquidityQuote({ depositAmounts: baseAmounts, decimals: [pool.assets[0].decimals, pool.assets[1].decimals], reserves: freshReserveAmounts, totalShare: refreshed.data?.total_share }); + const freshIsFirstProvider = poolType.supportsProvideLiquidity && freshInitial.isFirstProvider; + if (freshIsFirstProvider !== isFirstProvider) { + setReviewError("Pool liquidity changed while preparing review. Check the new pool state and review again."); + return; + } + const freshQuote = freshIsFirstProvider ? null : calculateProvideLiquidityQuote({ depositAmounts: baseAmounts, reserves: freshReserveAmounts, totalShare: refreshed.data?.total_share ?? "0" }); + if (!freshIsFirstProvider && (!freshQuote || !freshQuote.isProportional)) { + setReviewError("The refreshed reserve ratio no longer matches these deposit amounts. Adjust the amount and review again."); + return; + } + const minLpToReceive = freshQuote ? applySlippageFloor(freshQuote.expectedLpAmount, slippageBps) : undefined; + const instruction = buildProvideLiquidityExecuteInstruction({ pool, amounts: [...baseAmounts] as [string, string], slippageTolerance: slippageBpsToMaxSpread(slippageBps), minLpToReceive }); + const networkFeeEstimate = await estimateExecuteNetworkFee(signerOrClient, walletAddress, [instruction]).catch(() => undefined); + setReview({ + amounts: [...baseAmounts] as [string, string], + minLpToReceive, + expectedLpAmount: freshQuote?.expectedLpAmount, + poolShare: freshQuote ? formatLpShareBps(freshQuote.poolShareBps) : "Approximately 100% before locked minimum liquidity", + slippageBps, + reserveVersion: `${freshReserveAmounts.join(":")}:${refreshed.data?.total_share ?? "0"}`, + isFirstProvider: freshIsFirstProvider, + networkFeeEstimate, + }); + }; + + const currentReserveVersion = reserveAmounts ? `${reserveAmounts.join(":")}:${reserves.data?.total_share ?? "0"}` : ""; + const reviewIsCurrent = Boolean(review + && review.amounts[0] === baseAmounts[0] + && review.amounts[1] === baseAmounts[1] + && review.slippageBps === slippageBps + && review.reserveVersion === currentReserveVersion); + + const confirmAddLiquidity = () => { + if (!review || !reviewIsCurrent || provideTx.isPending) return; + provideTx.mutate({ pool, amounts: review.amounts, slippageTolerance: slippageBpsToMaxSpread(review.slippageBps), minLpToReceive: review.minLpToReceive }); + setReview(undefined); + }; + + return ( + + + + {isFirstProvider ? "Seed initial liquidity" : "Add liquidity"} + {poolType.provideCopy} + + + + + + {pool.assets.map((asset, index) => ( + updateAmount(index as 0 | 1, nextAmount)} + onMax={() => undefined} + onHalf={() => undefined} + disabled={provideTx.isPending || !poolType.supportsProvideLiquidity} + fiatHint={Reserve: {reserveAmounts ? `${formatAmount(reserveAmounts[index], asset.decimals)} ${asset.symbol}` : "loading…"}} + /> + ))} + + + {isFirstProvider ? ( + <> + Initial price: {initialQuote.price0In1 && initialQuote.price1In0 ? `1 ${pool.assets[0].symbol} = ${initialQuote.price0In1} ${pool.assets[1].symbol} · 1 ${pool.assets[1].symbol} = ${initialQuote.price1In0} ${pool.assets[0].symbol}` : "Enter both amounts to preview the starting price"} + Expected LP tokens: Contract-calculated after broadcast + Pool share: First provider starts at ~100% before minimum-liquidity lock and later deposits + + ) : ( + <> + Expected LP tokens: {quote ? formatAmount(quote.expectedLpAmount, 6) : "—"} + Estimated pool share: {quote ? formatLpShareBps(quote.poolShareBps) : "—"} + Ratio impact: {quote ? `${formatLpShareBps(quote.imbalanceBps)} off pool ratio` : "—"} + Minimum LP after slippage: {minLpToReceive ? formatAmount(minLpToReceive, 6) : "—"} + + )} + + + {isFirstProvider ? ( + + First-provider warning +
    +
  • Your two amounts set the pool's initial price; there is no existing reserve ratio to auto-balance against.
  • +
  • This ratio is effectively irreversible once arbitrage and later liquidity arrive, and the pool may permanently lock minimum liquidity.
  • +
  • Thin starting liquidity can cause extreme slippage and makes the pool easier to move; seed only with an intentional price.
  • +
+ +
+ ) : null} + + {network.isWrongNetwork ? Transactions are blocked while your wallet is off Juno mainnet. : null} + + {validationError && wallet.status === "connected" && !network.isWrongNetwork ? {validationError} : null} + {reviewError ?

{reviewError}

: null} + + + setReview(undefined)} + onConfirm={confirmAddLiquidity} + /> +
+ ); +} diff --git a/frontend/src/components/liquidity/LpPositionPanel.test.tsx b/frontend/src/components/liquidity/LpPositionPanel.test.tsx new file mode 100644 index 000000000..07293a738 --- /dev/null +++ b/frontend/src/components/liquidity/LpPositionPanel.test.tsx @@ -0,0 +1,118 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { LpPositionPanel } from "./LpPositionPanel"; + +const mocks = vi.hoisted(() => ({ + wallet: { status: "connected" as const, address: "juno1wallet", name: "Juno Wallet" }, + balances: { + data: [{ denom: "factory/juno1pair/astroport/share", amount: "50000000" }], + isLoading: false, + isError: false, + error: undefined as unknown, + refetch: vi.fn(), + }, + reserves: { + data: { + total_share: "1000000000", + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "5000000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "10000000000" }, + ], + }, + isLoading: false, + isError: false, + error: undefined as unknown, + refetch: vi.fn(), + }, +})); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => ({ wallet: mocks.wallet }), +})); + +vi.mock("../../queries/useWalletBalances", () => ({ + resolveDenom: () => ({ denom: pool.lpToken, symbol: "JUNO/TOKEN LP", decimals: 6, source: "lp" }), + getWalletBalanceAmount: (balances: Array<{ denom: string; amount: string }> | undefined, denom: string) => balances?.find((balance) => balance.denom === denom)?.amount, + useWalletBalances: () => mocks.balances, +})); + +vi.mock("../../queries/usePools", () => ({ + usePoolReserves: () => mocks.reserves, +})); + +const pool: RegistryPool = { + id: "juno-token", + label: "JUNO / TOKEN", + pair: "juno1pair", + lpToken: "factory/juno1pair/astroport/share", + type: "xyk", + feeBps: 30, + enabled: true, + status: "active", + explorer: "https://ping.pub/juno/wasm/contract/juno1pair", + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6 }, + { kind: "native", id: "factory/pair/token", symbol: "TOKEN", decimals: 6 }, + ], +}; + +function renderPanel() { + return render( + + + , + ); +} + +describe("LpPositionPanel", () => { + it("renders LP balance, share, underlying estimates, and quick actions", () => { + renderPanel(); + + expect(screen.getByText("Position found")).toBeTruthy(); + expect(screen.getByText("50 JUNO/TOKEN LP")).toBeTruthy(); + expect(screen.getByText("5.00%")).toBeTruthy(); + expect(screen.getByText("250 JUNO")).toBeTruthy(); + expect(screen.getByText("500 TOKEN")).toBeTruthy(); + expect(screen.getByRole("link", { name: "Add liquidity" }).getAttribute("href")).toBe("/pools/juno1pair"); + expect(screen.getByRole("link", { name: "Remove liquidity" }).getAttribute("href")).toBe("/pools/juno1pair"); + expect(screen.getByRole("link", { name: "Stake / claim" }).getAttribute("href")).toBe("/pools/juno1pair"); + }); + + it("shows wallet empty state when disconnected", () => { + mocks.wallet.status = "disconnected" as never; + renderPanel(); + expect(screen.getByText("Connect wallet to view LP position")).toBeTruthy(); + mocks.wallet.status = "connected"; + }); + + it("shows no-position empty state when LP balance is zero", () => { + mocks.balances.data = [{ denom: pool.lpToken, amount: "0" }]; + renderPanel(); + expect(screen.getByText("No LP balance for this pool")).toBeTruthy(); + mocks.balances.data = [{ denom: pool.lpToken, amount: "50000000" }]; + }); + + it("shows loading and error states", () => { + mocks.balances.isLoading = true; + const { rerender } = render( + + + , + ); + expect(screen.getByLabelText("Loading LP position")).toBeTruthy(); + + mocks.balances.isLoading = false; + mocks.reserves.isError = true; + mocks.reserves.error = new Error("RPC unavailable"); + rerender( + + + , + ); + expect(screen.getByRole("alert").textContent).toContain("Current pool balances could not be loaded"); + mocks.reserves.isError = false; + mocks.reserves.error = undefined; + }); +}); diff --git a/frontend/src/components/liquidity/LpPositionPanel.tsx b/frontend/src/components/liquidity/LpPositionPanel.tsx new file mode 100644 index 000000000..3d088d64e --- /dev/null +++ b/frontend/src/components/liquidity/LpPositionPanel.tsx @@ -0,0 +1,103 @@ +import { Link } from "react-router-dom"; +import type { RegistryPool } from "../../config/registry"; +import { formatAmount } from "../../lib/format/amounts"; +import { estimateLpPosition, formatPositionSharePercent } from "../../lib/liquidity/position"; +import { assessPoolRisk } from "../../lib/risk"; +import { usePoolReserves } from "../../queries/usePools"; +import { getWalletBalanceAmount, resolveDenom, useWalletBalances } from "../../queries/useWalletBalances"; +import { useWallet } from "../../wallet/WalletContext"; +import { EmptyState, ErrorState, RiskBadgeList, Skeleton } from "../common"; + +type LpPositionPanelProps = { + pool: RegistryPool; + compact?: boolean; + onAdd?: () => void; + onRemove?: () => void; + onStake?: () => void; +}; + +export function LpPositionPanel({ pool, compact = false, onAdd, onRemove, onStake }: LpPositionPanelProps) { + const { wallet } = useWallet(); + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const balances = useWalletBalances(walletAddress, [pool]); + const reserves = usePoolReserves(pool); + const lp = resolveDenom(pool.lpToken, [pool]); + const lpBalance = getWalletBalanceAmount(balances.data, pool.lpToken); + const isLoading = wallet.status === "connected" && (balances.isLoading || reserves.isLoading); + const hasError = balances.isError || reserves.isError; + const position = estimateLpPosition(reserves.data, lpBalance); + const poolHref = `/pools/${pool.pair}`; + const risk = assessPoolRisk(pool, reserves.data); + + return ( +
+
+
+

LP position

+

{pool.label}

+

{pool.assets.map((asset) => asset.symbol).join(" / ")} pool shares

+ {!compact ? : null} +
+ + {position.hasPosition ? "Position found" : "No LP balance"} + +
+ + {wallet.status !== "connected" ? ( + LP balances, pool share, and underlying token estimates require a connected wallet. + ) : hasError ? ( + { + void balances.refetch(); + void reserves.refetch(); + }} + /> + ) : isLoading ? ( +
+ + + +
+ ) : !position.hasPosition ? ( + + Your wallet does not currently hold {lp.symbol}. Add liquidity to mint LP shares for this pool. + + ) : ( + <> +
+
+ Wallet LP balance + {formatAmount(position.lpBalance, lp.decimals)} {lp.symbol} +
LP token identifier{pool.lpToken}
+
+
+ Pool share + {formatPositionSharePercent(position.shareBps)} + {formatAmount(position.totalShare, lp.decimals)} total LP +
+
+
+ {pool.assets.map((asset, index) => ( +
+
{asset.symbol} underlying estimate
+
+ {position.underlyingAssets[index] + ? `${formatAmount(position.underlyingAssets[index].amount, asset.decimals)} ${asset.symbol}` + : "—"} +
+
+ ))} +
+ + )} + +
+ {onAdd ? : Add liquidity} + {onRemove ? : Remove liquidity} + {onStake ? : Stake / claim} +
+
+ ); +} diff --git a/frontend/src/components/liquidity/RemoveLiquidityForm.test.tsx b/frontend/src/components/liquidity/RemoveLiquidityForm.test.tsx new file mode 100644 index 000000000..e24df8f9b --- /dev/null +++ b/frontend/src/components/liquidity/RemoveLiquidityForm.test.tsx @@ -0,0 +1,134 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { ToastProvider } from "../common"; +import { RemoveLiquidityForm } from "./RemoveLiquidityForm"; + +const mocks = vi.hoisted(() => ({ + wallet: { + status: "connected" as const, + address: "juno1wallet", + signer: vi.fn(), + }, + network: { + expectedChainId: "juno-1" as const, + connectedChainId: "juno-1", + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }, + mutateAsync: vi.fn(), + refetch: vi.fn(), +})); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => ({ wallet: mocks.wallet }), + useNetworkGuard: () => ({ network: mocks.network, switchToJuno: vi.fn() }), +})); + +vi.mock("../../settings/SlippageSettingsContext", () => ({ + useSlippageSettings: () => ({ slippageBps: 50, formattedSlippagePercent: "0.5", maxSpread: "0.005" }), +})); + +vi.mock("../../queries/useWalletBalances", () => ({ + resolveDenom: () => ({ denom: pool.lpToken, symbol: "JUNO/TOKEN LP", decimals: 6, source: "lp" }), + getWalletBalanceAmount: () => "100000000", + useWalletBalances: () => ({ + data: [{ denom: pool.lpToken, amount: "100000000" }], + isError: false, + error: undefined, + }), +})); + +vi.mock("../../queries/usePools", () => ({ + usePoolReserves: () => ({ + data: { + total_share: "1000000000", + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "5000000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "10000000000" }, + ], + }, + isFetching: false, + isError: false, + error: undefined, + refetch: mocks.refetch, + }), +})); + +vi.mock("../../mutations/useWithdrawLiquidityTx", () => ({ + buildWithdrawLiquidityExecuteInstruction: () => ({ contractAddress: "juno1pair", msg: {} }), + useWithdrawLiquidityTx: () => ({ isPending: false, mutateAsync: mocks.mutateAsync, txState: { status: "idle", label: "Ready" } }), +})); + +const pool: RegistryPool = { + id: "juno-token", + label: "JUNO / TOKEN", + pair: "juno1pair", + lpToken: "factory/juno1pair/astroport/share", + type: "xyk", + feeBps: 30, + enabled: true, + status: "active", + verified: true, + source: "registry", + explorer: "https://ping.pub/juno/wasm/contract/juno1pair", + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, verified: true }, + { kind: "native", id: "factory/pair/token", symbol: "TOKEN", decimals: 6, verified: true }, + ], +}; + +function renderForm() { + return render( + + + , + ); +} + +describe("RemoveLiquidityForm", () => { + beforeEach(() => { + mocks.mutateAsync.mockReset(); + mocks.refetch.mockReset(); + mocks.refetch.mockResolvedValue({ + data: { + total_share: "1000000000", + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "5000000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "10000000000" }, + ], + }, + }); + }); + + it("fills LP amount from quick-fill percentages and updates underlying estimates", () => { + const { container } = renderForm(); + + fireEvent.click(screen.getByRole("button", { name: "50%" })); + + expect((screen.getByLabelText("LP amount amount") as HTMLInputElement).value).toBe("50"); + expect(container.textContent).toContain("250 / 248.75 JUNO"); + expect(container.textContent).toContain("500 / 497.5 TOKEN"); + expect((screen.getByRole("button", { name: "Review withdrawal" }) as HTMLButtonElement).disabled).toBe(false); + }); + + it("passes LP amount and slippage-protected minimum assets to the withdraw mutation", async () => { + mocks.mutateAsync.mockResolvedValueOnce({ transactionHash: "ABC123" }); + renderForm(); + + fireEvent.click(screen.getByRole("button", { name: "50%" })); + fireEvent.click(screen.getByRole("button", { name: "Review withdrawal" })); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + await waitFor(() => expect(mocks.mutateAsync).toHaveBeenCalledWith({ + pool, + lpAmount: "50000000", + minAssetsToReceive: [ + { info: { native_token: { denom: "ujuno" } }, amount: "248750000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "497500000" }, + ], + })); + }); +}); diff --git a/frontend/src/components/liquidity/RemoveLiquidityForm.tsx b/frontend/src/components/liquidity/RemoveLiquidityForm.tsx new file mode 100644 index 000000000..d8e630a46 --- /dev/null +++ b/frontend/src/components/liquidity/RemoveLiquidityForm.tsx @@ -0,0 +1,220 @@ +import { useMemo, useState } from "react"; +import type { RegistryPool } from "../../config/registry"; +import { formatAmount, isBaseAmountGreaterThan, parseTokenAmount } from "../../lib/format/amounts"; +import { applySlippageToAssets, calculatePercentageFill, estimateWithdrawAssets } from "../../lib/liquidity/withdraw"; +import { getPoolTypeMetadata } from "../../lib/pools/poolTypes"; +import { assessPoolRisk } from "../../lib/risk"; +import { formatBpsPercent } from "../../lib/swap/slippage"; +import { buildWithdrawLiquidityExecuteInstruction, useWithdrawLiquidityTx } from "../../mutations/useWithdrawLiquidityTx"; +import { estimateExecuteNetworkFee, type NetworkFeeEstimate } from "../../lib/cosmjs/fees"; +import { usePoolReserves } from "../../queries/usePools"; +import { getWalletBalanceAmount, resolveDenom, useWalletBalances } from "../../queries/useWalletBalances"; +import { useSlippageSettings } from "../../settings/SlippageSettingsContext"; +import { useNetworkGuard, useWallet } from "../../wallet/WalletContext"; +import { RiskAcknowledgement, RiskBadgeList, TokenAmountInput, TransactionReview } from "../common"; +import { TxStatusDialog } from "../tx/TxStatusDialog"; + +const QUICK_FILL_PERCENTAGES = [25, 50, 75, 100] as const; +type WithdrawAssets = ReturnType; +type RemoveLiquidityReview = { + lpAmount: string; + expectedAssets: WithdrawAssets; + minimumAssets: WithdrawAssets; + slippageBps: number; + reserveVersion: string; + networkFeeEstimate?: NetworkFeeEstimate; +}; + +function isPositiveBaseAmount(amount: string) { + return /^\d+$/.test(amount) && BigInt(amount) > 0n; +} + +export function RemoveLiquidityForm({ pool }: { pool: RegistryPool }) { + const { wallet } = useWallet(); + const { network } = useNetworkGuard(); + const [amount, setAmount] = useState(""); + const [riskAcknowledged, setRiskAcknowledged] = useState(false); + const [review, setReview] = useState(); + const [isPreparingReview, setIsPreparingReview] = useState(false); + const [reviewError, setReviewError] = useState(); + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const balances = useWalletBalances(walletAddress, [pool]); + const reserves = usePoolReserves(pool); + const { slippageBps } = useSlippageSettings(); + const lp = resolveDenom(pool.lpToken, [pool]); + const lpBalance = getWalletBalanceAmount(balances.data, pool.lpToken); + const parsedAmount = parseTokenAmount(amount, lp.decimals); + const lpBaseAmount = parsedAmount.baseAmount; + const expectedAssets = useMemo(() => estimateWithdrawAssets(reserves.data, lpBaseAmount), [lpBaseAmount, reserves.data]); + const minAssetsToReceive = useMemo(() => applySlippageToAssets(expectedAssets, slippageBps), [expectedAssets, slippageBps]); + const risk = assessPoolRisk(pool, reserves.data); + const poolType = getPoolTypeMetadata(pool.type); + const signerOrClient = wallet.status === "connected" ? wallet.signer : undefined; + const withdraw = useWithdrawLiquidityTx(signerOrClient, walletAddress); + + const hasAmount = parsedAmount.isValid && isPositiveBaseAmount(lpBaseAmount); + const exceedsBalance = Boolean(lpBalance && parsedAmount.isValid && isBaseAmountGreaterThan(lpBaseAmount, lpBalance)); + const canWithdraw = wallet.status === "connected" + && network.isJunoReady + && !network.isWrongNetwork + && hasAmount + && !exceedsBalance + && expectedAssets.length > 0 + && !reserves.isError + && !risk.blocked + && (!risk.requiresAcknowledgement || riskAcknowledged) + && !isPreparingReview + && !withdraw.isPending; + + const actionCopy = network.isWrongNetwork + ? "Switch to Juno to withdraw" + : wallet.status !== "connected" + ? "Connect wallet to withdraw" + : !hasAmount + ? "Enter LP amount" + : risk.blocked + ? "Pool or asset blocked" + : exceedsBalance + ? "Insufficient LP balance" + : reserves.isError + ? "Reserve query unavailable" + : reserves.isFetching && expectedAssets.length === 0 + ? "Estimating withdrawal…" + : risk.requiresAcknowledgement && !riskAcknowledged + ? "Acknowledge unverified pool" + : withdraw.isPending + ? "Withdrawing…" + : isPreparingReview + ? "Refreshing reserves…" + : "Review withdrawal"; + + const setBaseAmount = (baseAmount: string) => { + const displayValue = formatAmount(baseAmount, lp.decimals, lp.decimals).replace(/,/g, ""); + setAmount(displayValue === "0" ? "" : displayValue); + }; + + const prepareWithdraw = async () => { + if (!canWithdraw) return; + setIsPreparingReview(true); + setReviewError(undefined); + const refreshed = await reserves.refetch(); + setIsPreparingReview(false); + if (!refreshed.data) { + setReviewError("Fresh pool reserves could not be loaded. Review remains unavailable."); + return; + } + const freshExpectedAssets = estimateWithdrawAssets(refreshed.data, lpBaseAmount); + if (freshExpectedAssets.length === 0) { + setReviewError("Withdrawal outputs could not be estimated from refreshed reserves."); + return; + } + const minimumAssets = applySlippageToAssets(freshExpectedAssets, slippageBps); + const instruction = buildWithdrawLiquidityExecuteInstruction({ pool, lpAmount: lpBaseAmount, minAssetsToReceive: minimumAssets }); + const networkFeeEstimate = await estimateExecuteNetworkFee(signerOrClient, walletAddress, [instruction]).catch(() => undefined); + setReview({ + lpAmount: lpBaseAmount, + expectedAssets: freshExpectedAssets, + minimumAssets, + slippageBps, + reserveVersion: `${refreshed.data.assets.map((asset) => asset.amount).join(":")}:${refreshed.data.total_share}`, + networkFeeEstimate, + }); + }; + + const currentReserveVersion = reserves.data ? `${reserves.data.assets.map((asset) => asset.amount).join(":")}:${reserves.data.total_share}` : ""; + const reviewIsCurrent = Boolean(review && review.lpAmount === lpBaseAmount && review.slippageBps === slippageBps && review.reserveVersion === currentReserveVersion); + + const handleWithdraw = async () => { + if (!review || !reviewIsCurrent || withdraw.isPending) return; + try { + await withdraw.mutateAsync({ pool, lpAmount: review.lpAmount, minAssetsToReceive: review.minimumAssets }); + setAmount(""); + setReview(undefined); + } catch { /* Shared transaction runner owns failure state and recovery copy. */ } + }; + + return ( +
+

Remove liquidity

+

{poolType.withdrawCopy}

+ + setAmount(nextAmount)} + onMax={setBaseAmount} + disabled={wallet.status !== "connected" || network.isWrongNetwork || withdraw.isPending} + /> +
+ {QUICK_FILL_PERCENTAGES.map((percent) => ( + + ))} +
+
+
Wallet LP balance
{lpBalance ? `${formatAmount(lpBalance, lp.decimals)} ${lp.symbol}` : wallet.status === "connected" ? "Loading…" : "Connect wallet"}
+ {pool.assets.map((asset, index) => { + const expected = expectedAssets[index]?.amount; + const minimum = minAssetsToReceive[index]?.amount; + return ( +
+
{asset.symbol} expected / minimum ({formatBpsPercent(slippageBps)})
+
+ {expected ? `${formatAmount(expected, asset.decimals)} / ${formatAmount(minimum, asset.decimals)} ${asset.symbol}` : "—"} +
+
+ ); + })} +
+
LP token identifier{pool.lpToken}
+ {!poolType.supportsWithdrawSimulation ? ( +

{poolType.shortLabel} withdraw simulation is not implemented locally. The amounts above are proportional estimates from live reserves; verify final outputs in the wallet before signing.

+ ) : null} + {balances.isError ?

Your LP balance could not be loaded. Try again before removing liquidity.

: null} + {reserves.isError ?

Current pool balances could not be loaded. Output estimates are unavailable; try again.

: null} + {network.isWrongNetwork ?

Switch to Juno to withdraw liquidity. Transactions are blocked off-network.

: null} + {reviewError ?

{reviewError}

: null} + + {wallet.status !== "connected" ?

Connect a wallet to see your position and remove liquidity.

: null} + + + [ + { label: `${asset.symbol} receive · estimated`, value: `${formatAmount(review.expectedAssets[index]?.amount ?? "0", asset.decimals)} ${asset.symbol}` }, + { label: `${asset.symbol} minimum · enforced`, value: `${formatAmount(review.minimumAssets[index]?.amount ?? "0", asset.decimals)} ${asset.symbol}` }, + ]), + { label: "Slippage tolerance · enforced", value: formatBpsPercent(review.slippageBps) }, + { label: "Price impact", value: poolType.supportsWithdrawSimulation ? "Included in contract simulation" : "Unavailable; proportional reserve estimate only", tone: poolType.supportsWithdrawSimulation ? "default" as const : "warning" as const }, + { label: "Pool status", value: `${pool.status}${pool.verified === true ? ", verified" : ", unverified"}` }, + ] : []} + disclosures={[ + { label: "Pair contract", value: pool.pair }, + { label: "LP token", value: pool.lpToken }, + ...pool.assets.map((asset) => ({ label: `${asset.symbol} identifier`, value: asset.id })), + ]} + warning={!reviewIsCurrent && review ? "LP amount, slippage, or reserves changed. Close this review and prepare a new one." : !poolType.supportsWithdrawSimulation ? "Output impact is unavailable for this pool type. Verify the enforced minimums carefully before signing." : undefined} + confirmDisabled={!reviewIsCurrent} + pending={withdraw.isPending} + onClose={() => setReview(undefined)} + onConfirm={() => void handleWithdraw()} + /> +
+ ); +} diff --git a/frontend/src/components/pools/PoolDetailPage.test.tsx b/frontend/src/components/pools/PoolDetailPage.test.tsx new file mode 100644 index 000000000..0fb25e1c7 --- /dev/null +++ b/frontend/src/components/pools/PoolDetailPage.test.tsx @@ -0,0 +1,154 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { PoolDetailPage } from "./PoolDetailPage"; + +const mocks = vi.hoisted(() => ({ + metrics: undefined as Record | undefined, + access: undefined as { source: "indexer" | "mock" | "fallback" | "disabled"; isFallback: boolean; isMock: boolean; isStale: boolean; error?: { code: string; message: string } } | undefined, + reserves: { + isLoading: false, + isFetching: false, + isError: false, + error: undefined as unknown, + data: { assets: [{ amount: "100000000" }, { amount: "250000000" }], total_share: "50000000" }, + refetch: vi.fn(), + }, +})); + +const pool: RegistryPool = { + id: "juno-usdc", + label: "JUNO / USDC", + pair: "juno1pooldetail", + lpToken: "factory/juno1pooldetail/astroport/share", + type: "xyk", + feeBps: 30, + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", name: "Juno", decimals: 6, logoURI: "https://example.com/juno.svg", verified: true }, + { kind: "ibc", id: "ibc/usdc", symbol: "USDC", name: "USD Coin", decimals: 6, logoURI: "https://example.com/usdc.svg", denomTrace: "transfer/channel-42/uusdc", verified: true }, + ], + explorer: "https://ping.pub/juno/wasm/contract/juno1pooldetail", + enabled: true, + status: "active", + verified: true, + source: "registry", + notes: "Test pool", +}; + +vi.mock("../../queries/useDexRegistry", () => ({ + useDexRegistry: () => ({ + registry: { explorerBaseUrl: "https://ping.pub/juno" }, + pools: [pool], + discovery: { isError: false }, + }), +})); + +vi.mock("../../queries/usePools", () => ({ + usePoolMetrics: () => ({ data: mocks.metrics, access: mocks.access, isError: false }), + usePoolReserves: () => mocks.reserves, + usePoolCandles: () => ({ data: [], access: mocks.access, isLoading: false, isFetching: false, refetch: vi.fn() }), + usePoolActivity: () => ({ data: [], access: mocks.access, isLoading: false }), +})); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => ({ wallet: { status: "connected", address: "juno1wallet", name: "Test wallet" } }), +})); + +vi.mock("../liquidity/AddLiquidityForm", () => ({ + AddLiquidityForm: () =>
Add liquidity form
, +})); + +vi.mock("../liquidity/RemoveLiquidityForm", () => ({ + RemoveLiquidityForm: () =>
Remove liquidity form
, +})); + +vi.mock("../liquidity/LpPositionPanel", () => ({ + LpPositionPanel: ({ onAdd }: { onAdd?: () => void }) => ( +
+ LP position panel + +
+ ), +})); + +vi.mock("../incentives/IncentivesPanel", () => ({ + IncentivesPanel: () =>
Incentives panel
, +})); + +function renderDetail() { + return render( + + + } /> + + , + ); +} + +describe("PoolDetailPage", () => { + beforeEach(() => { + mocks.metrics = undefined; + mocks.access = undefined; + mocks.reserves = { + isLoading: false, + isFetching: false, + isError: false, + error: undefined, + data: { assets: [{ amount: "100000000" }, { amount: "250000000" }], total_share: "50000000" }, + refetch: vi.fn(), + }; + }); + + it("renders per-pool analytics, reserves, actions, and status", () => { + mocks.metrics = { + [pool.pair]: { tvlUsd: 125000, volume24hUsd: 42000, feeApr: 4.5, incentivesApr: 1.25, totalApr: 5.75, incentivized: true }, + }; + mocks.access = { source: "indexer", isFallback: false, isMock: false, isStale: false }; + + renderDetail(); + + expect(screen.getByRole("heading", { name: "JUNO / USDC" })).toBeTruthy(); + expect(screen.getByText("$125,000")).toBeTruthy(); + expect(screen.getByText("$42,000")).toBeTruthy(); + expect(screen.getByText("5.75%")).toBeTruthy(); + expect(screen.getAllByText(/XYK/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText(/30 bps fee/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getByText("50")).toBeTruthy(); + expect(screen.getByText(/100 JUNO/i)).toBeTruthy(); + expect(screen.getByText(/250 USDC/i)).toBeTruthy(); + expect(screen.getByText(/1 JUNO ≈ 2.5 USDC/i)).toBeTruthy(); + expect(screen.getByText(/Your pool percentage is your LP balance divided by all LP shares/i)).toBeTruthy(); + expect(screen.getByRole("link", { name: /back to pools/i }).getAttribute("href")).toBe("/pools"); + expect(screen.getByText("Technical pool details")).toBeTruthy(); + const position = screen.getByText("LP position panel"); + const performance = screen.getByRole("heading", { name: "Performance" }); + expect(position.compareDocumentPosition(performance) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Add liquidity" })); + expect(screen.getByText("Add liquidity form")).toBeTruthy(); + }); + + it("shows honest unavailable-metrics copy without fake TVL, volume, APR, charts, or transactions", () => { + renderDetail(); + + const analytics = screen.getByLabelText("Pool analytics cards"); + expect(within(analytics).getAllByText("Metrics unavailable").length).toBeGreaterThanOrEqual(3); + expect(screen.getByText(/No price history yet/i)).toBeTruthy(); + expect(screen.getByText(/No swap, add, withdraw, or claim activity was returned/i)).toBeTruthy(); + expect(screen.getByText(/USD value, volume, and APR require market data/i)).toBeTruthy(); + }); + + it("renders Juno-denominated TVL and volume when USD pricing is unavailable", () => { + mocks.metrics = { + [pool.pair]: { tvlUsd: null, tvlJuno: 1250, volume24hUsd: null, volume24hJuno: 42.5 }, + }; + mocks.access = { source: "indexer", isFallback: false, isMock: false, isStale: false }; + + renderDetail(); + + expect(screen.getByText("1,250 JUNO")).toBeTruthy(); + expect(screen.getByText("42.5 JUNO")).toBeTruthy(); + expect(screen.queryByText("Requires pricing data")).toBeNull(); + expect(screen.queryByText("Requires volume data")).toBeNull(); + }); +}); diff --git a/frontend/src/components/pools/PoolDetailPage.tsx b/frontend/src/components/pools/PoolDetailPage.tsx new file mode 100644 index 000000000..a13acc462 --- /dev/null +++ b/frontend/src/components/pools/PoolDetailPage.tsx @@ -0,0 +1,190 @@ +import { Link, useParams } from "react-router-dom"; +import { useState } from "react"; +import type { RegistryAsset, RegistryPool } from "../../config/registry"; +import type { PoolResponse } from "../../lib/astroport/queries"; +import { formatAmount } from "../../lib/format/amounts"; +import { assessPoolRisk } from "../../lib/risk"; +import { getPoolTotalApr } from "../../lib/pools/poolList"; +import type { PoolMetrics } from "../../lib/pools/poolList"; +import { getPoolTypeMetadata } from "../../lib/pools/poolTypes"; +import { useDexRegistry } from "../../queries/useDexRegistry"; +import { usePoolActivity, usePoolMetrics, usePoolReserves } from "../../queries/usePools"; +import { PriceCandleChart } from "../charts/PriceCandleChart"; +import { Modal, RiskBadgeList, TokenLogo } from "../common"; +import { IncentivesPanel } from "../incentives/IncentivesPanel"; +import { AddLiquidityForm } from "../liquidity/AddLiquidityForm"; +import { LpPositionPanel } from "../liquidity/LpPositionPanel"; +import { RemoveLiquidityForm } from "../liquidity/RemoveLiquidityForm"; +import { WalletTransactionHistory } from "../wallet/WalletTransactionHistory"; + +export function PoolDetailPage() { + const { pairAddress } = useParams(); + const { pools, discovery, registry } = useDexRegistry(); + const [manageAction, setManageAction] = useState<"add" | "remove" | "stake" | null>(null); + const pool = pools.find((candidate) => candidate.pair === pairAddress); + const poolMetrics = usePoolMetrics(pool ? [pool] : []); + const poolActivity = usePoolActivity(pool, 50); + const reserves = usePoolReserves(pool); + const metrics = pool ? poolMetrics.data?.[pool.pair] : undefined; + const poolType = pool ? getPoolTypeMetadata(pool.type) : undefined; + + if (!pool) { + return

Pool not found

This pool is not in the current pool list.{discovery.isError ? " Some pools could not be loaded, so try again before concluding it is unavailable." : ""}

Back to pools
; + } + + const risk = assessPoolRisk(pool, reserves.data); + + return ( +
+
+
+

Pool · {pool.assets.map((asset) => asset.symbol).join(" / ")}

+

{pool.label}

+

{poolType?.label ?? pool.type.toUpperCase()} · {pool.feeBps} bps fee

+ +
+ ← Back to pools +
+ +
setManageAction("add")} onRemove={() => setManageAction("remove")} onStake={() => setManageAction("stake")} />
+ + setManageAction(null)}> + setManageAction(null)}> + setManageAction(null)}> + +
+

Performance

+
+ + + +
+ {!metrics ? ( +

TVL, 24h volume, and APR are unavailable for this pool.

+ ) : null} + {metrics && poolMetrics.access?.updatedAt ?

{poolMetrics.access.isStale ? "Last available" : "Updated"} {formatDataTime(poolMetrics.access.updatedAt)}

: null} +
+ +
+

Pool reserves

+ {reserves.isError ?

Current balances are unavailable. Position estimates and reserves may be incomplete.

: null} +
+ {pool.assets.map((asset, index) => ( + + ))} + +
+
+ +
+ +
+ +
+ +
+ +
+ Technical pool details +
+
Pool contract{pool.pair}
+
LP token{pool.lpToken}
+
+
+
Total LP shares
{reserves.data ? formatAmount(reserves.data.total_share, 6) : "Unavailable"}
+
How estimates work
Your pool percentage is your LP balance divided by all LP shares. Estimated token amounts use that percentage of each pool balance.
+
Pool model
{poolType?.label ?? pool.type.toUpperCase()} · {pool.feeBps} bps fee. {typeSpecificCopy(pool)}
+
Data limits
USD value, volume, and APR require market data and are not inferred from token balances. {poolType?.withdrawCopy}
+
+
+
+ ); +} + +function MetricCard({ label, value, hint }: { label: string; value: string; hint?: string }) { + const muted = value === "Metrics unavailable" || value === "—" || value === "Unavailable"; + const long = value.length > 22; + const valueClass = [muted ? "metric-value-muted" : "", long ? "metric-value-long" : ""].filter(Boolean).join(" ") || undefined; + return
{label}{value}{hint ? {hint} : null}
; +} + +function ReserveCard({ asset, index, reserves }: { asset: RegistryAsset; index: number; reserves: PoolResponse | undefined }) { + const reserve = reserves?.assets[index]?.amount; + return ( +
+ {asset.name ?? asset.symbol} + {reserve ? `${formatAmount(reserve, asset.decimals)} ${asset.symbol}` : "—"} +
Asset identifier{asset.id}{asset.denomTrace ? {asset.denomTrace} : null}
+
+ ); +} + +function typeSpecificCopy(pool: RegistryPool) { + const metadata = getPoolTypeMetadata(pool.type); + if (pool.type === "stable") return `${metadata.detailCopy} The amplification setting is not available from this pool's current data.`; + if (pool.type === "concentrated") return `${metadata.detailCopy} Concentration settings are not available from this pool's current data.`; + return metadata.detailCopy; +} + +function formatCurrentPrice(pool: RegistryPool, reserves: PoolResponse | undefined) { + if (!reserves) return "—"; + const base = normalizedNumber(reserves.assets[0]?.amount, pool.assets[0].decimals); + const quote = normalizedNumber(reserves.assets[1]?.amount, pool.assets[1].decimals); + if (!Number.isFinite(base) || !Number.isFinite(quote) || base <= 0 || quote <= 0) return "Unavailable"; + return `1 ${pool.assets[0].symbol} ≈ ${formatRatio(quote / base)} ${pool.assets[1].symbol}`; +} + +function normalizedNumber(amount: string | undefined, decimals: number) { + if (!amount || !/^\d+$/.test(amount)) return 0; + return Number(amount) / 10 ** decimals; +} + +function formatRatio(value: number) { + return new Intl.NumberFormat("en-US", { maximumSignificantDigits: 6 }).format(value); +} + +function formatUsd(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(value); +} + +function formatJuno(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value)} JUNO`; +} + +function formatMarketValue(usdValue: number | null | undefined, junoValue: number | null | undefined) { + return formatUsd(usdValue) ?? formatJuno(junoValue); +} + +function hasMarketValue(usdValue: number | null | undefined, junoValue: number | null | undefined) { + return formatMarketValue(usdValue, junoValue) !== undefined; +} + +function formatApr(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value)}%`; +} + +function aprHint(metrics: PoolMetrics) { + const parts = []; + if (typeof metrics.feeApr === "number") parts.push(`fees ${formatApr(metrics.feeApr)}`); + if (typeof metrics.incentivesApr === "number") parts.push(`incentives ${formatApr(metrics.incentivesApr)}`); + return parts.length > 0 ? parts.join(" + ") : "Market data"; +} + +function formatDataTime(value: string) { + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return "at an unknown time"; + return parsed.toLocaleString(); +} diff --git a/frontend/src/components/pools/PoolTable.test.tsx b/frontend/src/components/pools/PoolTable.test.tsx new file mode 100644 index 000000000..5dab1e19a --- /dev/null +++ b/frontend/src/components/pools/PoolTable.test.tsx @@ -0,0 +1,172 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { MemoryRouter, useLocation } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { PoolTable } from "./PoolTable"; + +const mocks = vi.hoisted(() => ({ + metrics: undefined as Record | undefined, + access: undefined as { source: "indexer" | "mock" | "fallback" | "disabled"; isFallback: boolean; isMock: boolean; isStale: boolean; error?: { code: string; message: string } } | undefined, + metricRefetch: vi.fn(), +})); + +vi.mock("../../queries/usePools", () => ({ + usePoolMetrics: () => ({ data: mocks.metrics, access: mocks.access, isError: false, refetch: mocks.metricRefetch }), + usePoolReserves: () => ({ + isLoading: false, + isError: false, + data: { assets: [{ amount: "1000000" }, { amount: "2000000" }], total_share: "1000000" }, + refetch: vi.fn(), + }), + usePoolCandles: () => ({ data: [], access: mocks.access, isLoading: false, isFetching: false, refetch: vi.fn() }), +})); + +vi.mock("../../queries/useWalletBalances", async () => { + const actual = await vi.importActual("../../queries/useWalletBalances"); + return { + ...actual, + useWalletBalances: () => ({ data: [] }), + }; +}); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => ({ wallet: { status: "idle" } }), +})); + +function pool(overrides: Partial & Pick): RegistryPool { + const [leftRaw, rightRaw] = overrides.label.split("/"); + const left = leftRaw?.trim(); + const right = rightRaw?.trim(); + return { + lpToken: `${overrides.id}-lp`, + assets: [ + { kind: "native", id: `${overrides.id}-base`, symbol: left ?? "AAA", name: left, decimals: 6, logoURI: `https://example.com/${left}.svg` }, + { kind: "native", id: `${overrides.id}-quote`, symbol: right ?? "BBB", name: `${right} on Juno`, decimals: 6, logoURI: `https://example.com/${right}.svg`, denomTrace: `transfer/channel-1/${right?.toLowerCase()}` }, + ], + explorer: `https://example.com/${overrides.pair}`, + enabled: true, + verified: true, + source: "registry", + notes: "Test pool", + ...overrides, + status: overrides.status ?? "active", + }; +} + +const pools = [ + pool({ id: "juno-usdc", label: "JUNO / USDC", pair: "juno1alpha", type: "xyk", feeBps: 30 }), + pool({ id: "atom-usdc", label: "ATOM / USDC", pair: "juno1beta", type: "stable", feeBps: 5, verified: false }), +]; + +function renderPoolTable() { + return render( + + + + , + ); +} + +function LocationProbe() { + const location = useLocation(); + return {location.pathname}; +} + +describe("PoolTable", () => { + beforeEach(() => { + mocks.metrics = undefined; + mocks.access = undefined; + mocks.metricRefetch.mockReset(); + }); + + it("renders compact pool identity with token logos and no visible pool tags", () => { + renderPoolTable(); + + expect(screen.getByAltText("JUNO logo").getAttribute("src")).toBe("https://example.com/JUNO.svg"); + expect(screen.getByText("JUNO / USDC")).toBeTruthy(); + const poolRows = screen.getAllByRole("row").slice(1); + for (const row of poolRows) { + expect(within(row).queryByText("XYK")).toBeNull(); + expect(within(row).queryByText(/XYK · 30 bps/i)).toBeNull(); + expect(within(row).queryByText(/verified pool/i)).toBeNull(); + } + expect(screen.queryByText("transfer/channel-1/usdc")).toBeNull(); + }); + + it("shows unavailable metric placeholders without pool list banner copy", () => { + renderPoolTable(); + + expect(screen.queryByText(/Browse pools by liquidity, volume, APR, type, and wallet position/i)).toBeNull(); + expect(screen.getAllByText("—").length).toBeGreaterThanOrEqual(3); + }); + + it("does not show a retry banner when pool metrics fail", () => { + mocks.access = { source: "fallback", isFallback: true, isMock: false, isStale: false, error: { code: "timeout", message: "indexer timed out" } }; + + renderPoolTable(); + expect(screen.queryByText("Pool metrics unavailable")).toBeNull(); + expect(screen.queryByRole("button", { name: /retry/i })).toBeNull(); + expect(mocks.metricRefetch).not.toHaveBeenCalled(); + }); + + it("navigates to pool details when a pool row is clicked", () => { + renderPoolTable(); + + fireEvent.click(screen.getByRole("row", { name: /open JUNO \/ USDC pool details/i })); + expect(screen.getByTestId("location").textContent).toBe("/pools/juno1alpha"); + expect(screen.queryByRole("link", { name: "Swap" })).toBeNull(); + expect(screen.queryByRole("link", { name: "Add" })).toBeNull(); + expect(screen.queryByRole("link", { name: "Details" })).toBeNull(); + }); + + it("filters rows by search and verification controls", () => { + renderPoolTable(); + + fireEvent.change(screen.getByLabelText(/search pools/i), { target: { value: "atom" } }); + expect(screen.getByText("ATOM / USDC")).toBeTruthy(); + expect(screen.queryByText("JUNO / USDC")).toBeNull(); + + fireEvent.change(screen.getByLabelText(/verification/i), { target: { value: "verified" } }); + expect(screen.getByText(/No pools match these filters/i)).toBeTruthy(); + }); + + it("sorts by TVL from indexer metrics", () => { + mocks.metrics = { + juno1alpha: { tvlUsd: 10, volume24hUsd: 5, totalApr: 1 }, + juno1beta: { tvlUsd: 500, volume24hUsd: 20, totalApr: 3 }, + }; + mocks.access = { source: "indexer", isFallback: false, isMock: false, isStale: false }; + renderPoolTable(); + + fireEvent.click(screen.getByRole("button", { name: /^TVL/ })); + const rows = screen.getAllByRole("row").slice(1); + expect(within(rows[0]).getByText("ATOM / USDC")).toBeTruthy(); + expect(within(rows[1]).getByText("JUNO / USDC")).toBeTruthy(); + expect(screen.getByText("$500")).toBeTruthy(); + expect(screen.getByText("3%")).toBeTruthy(); + }); + + it("exposes visible and programmatic sort direction on sortable columns", () => { + renderPoolTable(); + const tvlHeader = screen.getByRole("columnheader", { name: /tvl/i }); + expect(tvlHeader.getAttribute("aria-sort")).toBe("none"); + fireEvent.click(within(tvlHeader).getByRole("button")); + expect(tvlHeader.getAttribute("aria-sort")).toBe("descending"); + expect(within(tvlHeader).getByText("↓")).toBeTruthy(); + }); + + it("shows and sorts by Juno metrics when USD metrics are unavailable", () => { + mocks.metrics = { + juno1alpha: { tvlUsd: null, tvlJuno: 10, volume24hUsd: null, volume24hJuno: 5 }, + juno1beta: { tvlUsd: null, tvlJuno: 500, volume24hUsd: null, volume24hJuno: 20 }, + }; + mocks.access = { source: "indexer", isFallback: false, isMock: false, isStale: false }; + renderPoolTable(); + + fireEvent.click(screen.getByRole("button", { name: /^TVL/ })); + const rows = screen.getAllByRole("row").slice(1); + expect(within(rows[0]).getByText("ATOM / USDC")).toBeTruthy(); + expect(screen.getByText("500 JUNO")).toBeTruthy(); + expect(screen.getByText("20 JUNO")).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/pools/PoolTable.tsx b/frontend/src/components/pools/PoolTable.tsx new file mode 100644 index 000000000..e2962fc13 --- /dev/null +++ b/frontend/src/components/pools/PoolTable.tsx @@ -0,0 +1,173 @@ +import { useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import type { RegistryPool } from "../../config/registry"; +import { formatAmount } from "../../lib/format/amounts"; +import type { DataAccessState } from "../../lib/data-access/indexerFallback"; +import { + DEFAULT_POOL_LIST_CONTROLS, + filterAndSortPools, + getPoolTotalApr, + type PoolIncentiveFilter, + type PoolListControls, + type PoolListSortKey, + type PoolTypeFilter, + type PoolVerifiedFilter, +} from "../../lib/pools/poolList"; +import type { PoolMetrics } from "../../lib/pools/poolList"; +import { usePoolMetrics } from "../../queries/usePools"; +import { getWalletBalanceAmount, useWalletBalances, type WalletBalance } from "../../queries/useWalletBalances"; +import { useWallet } from "../../wallet/WalletContext"; +import { EmptyState, TokenLogo } from "../common"; + +export function PoolTable({ pools }: { pools: RegistryPool[] }) { + const [controls, setControls] = useState(DEFAULT_POOL_LIST_CONTROLS); + const metrics = usePoolMetrics(pools); + const wallet = useWallet(); + const walletAddress = wallet.wallet.status === "connected" ? wallet.wallet.address : undefined; + const balances = useWalletBalances(walletAddress, pools); + const visiblePools = useMemo( + () => filterAndSortPools(pools, controls, metrics.data ?? {}), + [controls, metrics.data, pools], + ); + + if (pools.length === 0) { + return Operators should add a real Juno pair to registry.juno-1.json and keep placeholders rejected by tests.; + } + + return ( +
+ +
+
+ + + + + Your position +
+ {visiblePools.map((pool) => ( + + ))} +
+ {visiblePools.length === 0 ? Try a different search term, pool type, verification, or incentive filter. : null} +
+ ); +} + +function PoolListControls({ controls, onChange }: { controls: PoolListControls; onChange: (controls: PoolListControls) => void }) { + return ( +
+ + + + +
+ ); +} + +function toggleSort(controls: PoolListControls, sortKey: PoolListSortKey): PoolListControls { + return { + ...controls, + sortKey, + sortDirection: controls.sortKey === sortKey && controls.sortDirection === "desc" ? "asc" : "desc", + }; +} + +function ariaSort(controls: PoolListControls, sortKey: PoolListSortKey): "ascending" | "descending" | "none" { + if (controls.sortKey !== sortKey) return "none"; + return controls.sortDirection === "asc" ? "ascending" : "descending"; +} + +function SortDirection({ controls, sortKey }: { controls: PoolListControls; sortKey: PoolListSortKey }) { + return ; +} + +function PoolRow({ pool, metrics, balances, access }: { pool: RegistryPool; metrics?: PoolMetrics; balances?: readonly WalletBalance[]; access?: DataAccessState }) { + const lpBalance = getWalletBalanceAmount(balances, pool.lpToken); + const apr = getPoolTotalApr(metrics); + return ( + +
+
+ +
+ {pool.label} +
+
+
+ + + +
+ Your position + {lpBalance && lpBalance !== "0" ? formatAmount(lpBalance, 6) : "No LP detected"} +
+ + ); +} + +function MetricCell({ label, value, metrics, access, tone }: { label: string; value: string | undefined; metrics?: PoolMetrics; access?: DataAccessState; tone?: "apr" }) { + return ( +
+ {label} + {value ?? "—"} + {value && access && !access.isStale ? live : null} +
+ ); +} + +function formatUsd(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(value); +} + +function formatJuno(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value)} JUNO`; +} + +function formatMarketValue(usdValue: number | null | undefined, junoValue: number | null | undefined) { + return formatUsd(usdValue) ?? formatJuno(junoValue); +} + +function formatApr(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value)}%`; +} diff --git a/frontend/src/components/pools/PoolsPage.tsx b/frontend/src/components/pools/PoolsPage.tsx new file mode 100644 index 000000000..ae8b9a72f --- /dev/null +++ b/frontend/src/components/pools/PoolsPage.tsx @@ -0,0 +1,21 @@ +import { Link } from "react-router-dom"; +import { useDexRegistry } from "../../queries/useDexRegistry"; +import { OptionalDataState, Skeleton } from "../common"; +import { PoolTable } from "./PoolTable"; + +export function PoolsPage() { + const { pools, discovery } = useDexRegistry(); + return ( +
+
+

Liquidity nodes · {pools.length}

+ + Create pool + +
+ {discovery.isError ? void discovery.refetch()}>Known pools remain available. : null} + {discovery.isFetching ?
: null} + +
+ ); +} diff --git a/frontend/src/components/portfolio/PortfolioPage.tsx b/frontend/src/components/portfolio/PortfolioPage.tsx new file mode 100644 index 000000000..a6005ae0d --- /dev/null +++ b/frontend/src/components/portfolio/PortfolioPage.tsx @@ -0,0 +1,236 @@ +import type { ReactNode } from "react"; +import { Link } from "react-router-dom"; +import { useQueries } from "@tanstack/react-query"; +import { queryPairPool } from "../../lib/astroport/queries"; +import { formatAmount } from "../../lib/format/amounts"; +import { truncateAddress } from "../../lib/format/addresses"; +import { queryIncentivesPoolState } from "../../lib/incentives"; +import { buildPortfolioSummary, totalLpBalance, type PortfolioPosition } from "../../lib/portfolio/portfolio"; +import { formatPositionSharePercent } from "../../lib/liquidity/position"; +import { useDexRegistry } from "../../queries/useDexRegistry"; +import { useWalletIndexerData } from "../../queries/usePools"; +import { useWalletBalances } from "../../queries/useWalletBalances"; +import { useWallet } from "../../wallet/WalletContext"; +import { EmptyState, ErrorState, OptionalDataState, Skeleton } from "../common"; +import { WalletTransactionHistory } from "../wallet/WalletTransactionHistory"; + +function usd(value: number | null) { + if (typeof value !== "number" || !Number.isFinite(value)) return "USD price unavailable"; + return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 2 }).format(value); +} + +function juno(value: number | null) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value)} JUNO`; +} + +function marketValue(usdValue: number | null, junoValue: number | null) { + return typeof usdValue === "number" && Number.isFinite(usdValue) ? usd(usdValue) : (juno(junoValue) ?? "Price unavailable"); +} + +function hasMarketValue(usdValue: number | null, junoValue: number | null) { + return typeof usdValue === "number" && Number.isFinite(usdValue) || typeof junoValue === "number" && Number.isFinite(junoValue); +} + +function positionStatus(position: PortfolioPosition) { + if (position.source === "indexer" || position.source === "mock") return "LP position"; + return "LP estimate"; +} + +function MetricValue({ muted = false, children }: { muted?: boolean; children: ReactNode }) { + return {children}; +} + +function PortfolioPositionCard({ position }: { position: PortfolioPosition }) { + const lpSymbol = `${position.pool.assets.map((asset) => asset.symbol).join("/")} LP`; + const hasStaked = BigInt(position.stakedLpBalance ?? "0") > 0n; + const hasRewards = position.rewards.some((reward) => reward.status === "claimable"); + + return ( +
+
+
+

{positionStatus(position)}

+

{position.pool.label}

+

{position.pool.assets.map((asset) => asset.symbol).join(" / ")} pool shares

+
+ Position found +
+
+
+ Total LP exposure + {formatAmount(totalLpBalance(position), 6)} {lpSymbol} +
LP token{position.pool.lpToken}
+
+
+ Pool share + {formatPositionSharePercent(position.shareBps)} + {position.shareBps > 0 ? "Based on current position data" : "Share unavailable"} +
+
+ Position value + {marketValue(position.valueUsd, position.valueJuno)} + {hasMarketValue(position.valueUsd, position.valueJuno) ? "Priced with market data" : "Not counted in aggregate total"} +
+
+ +
+
+
Unstaked LP
+
{formatAmount(position.lpBalance, 6)} {lpSymbol}
+
+
+
Staked LP
+
{hasStaked ? `${formatAmount(position.stakedLpBalance ?? "0", 6)} ${lpSymbol}` : "No staked balance reported"}
+
+ {position.assets.map((asset) => ( +
+
{asset.symbol} underlying
+
+ {formatAmount(asset.amount, asset.decimals)} {asset.symbol} + {hasMarketValue(asset.valueUsd, asset.valueJuno) ? · {marketValue(asset.valueUsd, asset.valueJuno)} : · price missing} +
+
+ ))} +
+
Claimable rewards
+
+ {hasRewards ? position.rewards.map((reward) => `${formatAmount(reward.amount, 6)} ${reward.symbol} (${marketValue(reward.valueUsd, reward.valueJuno)})`).join(", ") : "No claimable rewards reported"} +
+
+
+
+ Manage liquidity +
+
+ ); +} + +export function PortfolioPage() { + const { wallet } = useWallet(); + const { pools, discovery, registry } = useDexRegistry(); + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const balances = useWalletBalances(walletAddress, pools); + const indexerData = useWalletIndexerData(walletAddress); + const reserveQueries = useQueries({ + queries: pools.map((pool) => ({ + queryKey: ["portfolio-pool", pool.pair], + enabled: Boolean(walletAddress), + queryFn: () => queryPairPool(pool.pair), + staleTime: 30_000, + })), + }); + const incentivesQueries = useQueries({ + queries: pools.map((pool) => ({ + queryKey: ["portfolio-incentives", pool.lpToken, walletAddress], + enabled: Boolean(walletAddress), + queryFn: () => queryIncentivesPoolState(pool, walletAddress), + staleTime: 20_000, + retry: false, + })), + }); + const reservesByPair = Object.fromEntries(pools.map((pool, index) => [pool.pair, reserveQueries[index]?.data])); + const incentivesByLpToken = Object.fromEntries(pools.map((pool, index) => [pool.lpToken, incentivesQueries[index]?.data])); + const preferIndexer = Boolean(indexerData.access && !indexerData.access.isFallback && indexerData.data.positions.length > 0); + const portfolio = buildPortfolioSummary({ + pools, + balances: balances.data, + reservesByPair, + indexerPositions: indexerData.data.positions, + incentivesByLpToken, + preferIndexer, + }); + const reserveError = reserveQueries.find((query) => query.isError)?.error; + const isLoading = Boolean(walletAddress) && (balances.isLoading || indexerData.isLoading || reserveQueries.some((query) => query.isLoading) || incentivesQueries.some((query) => query.isLoading)); + return ( +
+
+
+

Portfolio

+

Wallet portfolio

+
+ {walletAddress ? ( +
+ + {wallet.name ?? truncateAddress(walletAddress)} +
+ ) : null} +
+ {discovery.isError ? void discovery.refetch()}>Known positions remain available. : null} + {!walletAddress ?

Connect a wallet to view LP positions, balances, rewards, and USD value.

: null} + {walletAddress && indexerData.access?.error ? void indexerData.refetch()}>Balances remain available; prices, rewards, or staked amounts may be incomplete. : null} + + {!walletAddress ? ( + Browse pools}> + LP balances and rewards are wallet-specific. The app remains read-only until a wallet is connected. + + ) : balances.isError || reserveError ? ( + { + void balances.refetch(); + reserveQueries.forEach((query) => void query.refetch()); + }} + /> + ) : isLoading ? ( +
+ + + +
+ ) : ( + <> +
+
+ Total LP value + {marketValue(portfolio.totalLpValueUsd, portfolio.totalLpValueJuno)} + {portfolio.missingPositionPrices ? `${portfolio.missingPositionPrices} position(s) missing prices` : "All positions priced"} +
+
+ Total claimable + {portfolio.claimableRewardCount ? marketValue(portfolio.totalClaimableUsd, portfolio.totalClaimableJuno) : "No rewards found"} + {portfolio.claimableRewardCount ? {portfolio.claimableRewardCount} reward row(s) : null} +
+
+ + {portfolio.positions.length === 0 ? ( + Explore pools}> + No LP balance was found for this wallet. Staked-only positions will appear when available. + + ) : ( +
+ {portfolio.positions.map((position) => )} +
+ )} + +
+
+
+

Wallet balances

+

{wallet.name ?? truncateAddress(walletAddress)} balances

+
+
+
+ {portfolio.walletBalances.filter((balance) => BigInt(balance.amount || "0") > 0n).slice(0, 12).map((balance) => ( +
+
{balance.symbol}
+
{formatAmount(balance.amount, balance.decimals)} {balance.source}
+
+ ))} + {portfolio.walletBalances.every((balance) => BigInt(balance.amount || "0") === 0n) ?
No non-zero known balances
: null} +
+
+ + + + )} +
+ ); +} diff --git a/frontend/src/components/settings/SettingsPanel.tsx b/frontend/src/components/settings/SettingsPanel.tsx new file mode 100644 index 000000000..5db1430bd --- /dev/null +++ b/frontend/src/components/settings/SettingsPanel.tsx @@ -0,0 +1,88 @@ +import { useEffect, useRef, useState } from "react"; +import { dexRegistry } from "../../config/registry"; +import { DANGEROUS_SLIPPAGE_BPS, HIGH_SLIPPAGE_BPS, MAX_SLIPPAGE_BPS, SLIPPAGE_PRESET_BPS, formatSlippagePercent } from "../../lib/swap/slippage"; +import { useSlippageSettings } from "../../settings/SlippageSettingsContext"; + +export function SettingsPanel({ onClose }: { onClose: () => void }) { + const panelRef = useRef(null); + const { slippageBps, setSlippageBps, setSlippagePercent, maxSpread } = useSlippageSettings(); + const isPreset = SLIPPAGE_PRESET_BPS.some((preset) => preset === slippageBps); + const [customValue, setCustomValue] = useState(isPreset ? "" : formatSlippagePercent(slippageBps)); + const maxSlippagePercent = MAX_SLIPPAGE_BPS / 100; + const customInvalid = customValue !== "" && (!Number.isFinite(Number(customValue)) || Number(customValue) <= 0 || Number(customValue) > maxSlippagePercent); + useEffect(() => { + const trigger = document.activeElement instanceof HTMLElement ? document.activeElement : undefined; + panelRef.current?.querySelector("button, input")?.focus(); + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + const onPointerDown = (event: PointerEvent) => { + if (event.target instanceof Node && !panelRef.current?.contains(event.target)) onClose(); + }; + document.addEventListener("keydown", onKeyDown); + document.addEventListener("pointerdown", onPointerDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + document.removeEventListener("pointerdown", onPointerDown); + trigger?.focus(); + }; + }, [onClose]); + + return ( +
+
+ Settings + +
+

Choose how much the price may move before your swap is cancelled. A lower value protects the quote but may cause more failed swaps in a moving market.

+
+ Slippage tolerance +
+ {SLIPPAGE_PRESET_BPS.map((preset) => ( + + ))} +
+ + {customInvalid ?

Enter slippage between 0.01% and {maxSlippagePercent}%.

: null} + {slippageBps > HIGH_SLIPPAGE_BPS ? ( +

= DANGEROUS_SLIPPAGE_BPS ? "error-text" : "field-error"} role="status"> + {slippageBps >= DANGEROUS_SLIPPAGE_BPS ? "Dangerously high" : "High"} slippage can expose this swap to a materially worse execution price. You will need to acknowledge it before review. +

+ ) : null} +
+
+ Technical settings +
+
Contract max spread
{maxSpread}
+
Network endpoint
{dexRegistry.rpcEndpoint}
+
+
+
+ ); +} diff --git a/frontend/src/components/swap/QuoteCard.test.tsx b/frontend/src/components/swap/QuoteCard.test.tsx new file mode 100644 index 000000000..59c0091e2 --- /dev/null +++ b/frontend/src/components/swap/QuoteCard.test.tsx @@ -0,0 +1,87 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { dexRegistry } from "../../config/registry"; +import type { RouteQuote } from "../../queries/useSwapQuote"; +import { QuoteCard } from "./QuoteCard"; + +vi.mock("../../queries/usePools", () => ({ + usePoolCandles: () => ({ + data: [], + access: { source: "indexer", isFallback: false, isMock: false, isStale: false }, + isLoading: false, + isFetching: false, + refetch: vi.fn(), + }), +})); + +describe("QuoteCard layout", () => { + it("keeps the route inside the collapsed details and marks values so long text can wrap", () => { + const pool = dexRegistry.pools[0]; + const askAsset = pool.assets[1]; + const quote: RouteQuote = { + offer_amount: "1000000", + return_amount: "123456789012345678901234567890", + spread_amount: "12345678901234567890", + commission_amount: "12345678901234567890", + source: "pair", + mode: "exact-in", + route: { + id: "direct", + hops: [{ pool, offerAsset: pool.assets[0], askAsset: pool.assets[1] }], + operations: [], + }, + }; + + render(); + + const routeLabel = screen.getAllByText("Route").find((element) => element.tagName === "DT"); + expect(routeLabel?.closest("dl")?.className).toBe("quote-rows"); + expect(routeLabel?.closest("details")).toBeTruthy(); + expect(screen.getByText(/JUNO → JUNOAGENT-TEST/i).closest("dd")?.className).toBe("quote-row-value route-value"); + }); + + it("shows the rate as the only always-visible line and collapses the detail rows", () => { + const pool = dexRegistry.pools[0]; + const quote: RouteQuote = { + offer_amount: "1000000", + return_amount: "1000000", + spread_amount: "1000", + commission_amount: "500", + source: "pair", + mode: "exact-in", + route: { + id: "stable-direct", + hops: [{ pool, offerAsset: pool.assets[0], askAsset: pool.assets[1] }], + operations: [], + }, + }; + + render(); + + const details = document.querySelector("details.quote-disclosure"); + expect(details).toBeTruthy(); + expect((details as HTMLDetailsElement).open).toBe(false); + expect(screen.getByText(/^1 JUNO = /)).toBeTruthy(); + expect(screen.getByText("Max slippage").closest("details")).toBe(details); + expect(screen.queryByText("Quote status")).toBeNull(); + }); + + it("reports the effective slippage read-only; the gear is the only place to change it", () => { + const pool = dexRegistry.pools[0]; + const quote: RouteQuote = { + offer_amount: "1000000", + return_amount: "990000", + spread_amount: "1000", + commission_amount: "500", + source: "pair", + mode: "exact-in", + route: { id: "direct", hops: [{ pool, offerAsset: pool.assets[0], askAsset: pool.assets[1] }], operations: [] }, + }; + + render(); + + expect(screen.getByText("2.37%")).toBeTruthy(); + expect(screen.queryByRole("group", { name: /max slippage preset/i })).toBeNull(); + expect(screen.queryByRole("button", { name: "0.5%" })).toBeNull(); + }); +}); diff --git a/frontend/src/components/swap/QuoteCard.tsx b/frontend/src/components/swap/QuoteCard.tsx new file mode 100644 index 000000000..d38e3dce8 --- /dev/null +++ b/frontend/src/components/swap/QuoteCard.tsx @@ -0,0 +1,120 @@ +import type { RouteQuote } from "../../queries/useSwapQuote"; +import type { RegistryAsset } from "../../config/registry"; +import { routeSymbols } from "../../lib/astroport/routes"; +import { formatAmount } from "../../lib/format/amounts"; +import { formatBpsPercent, getPriceImpact } from "../../lib/swap/slippage"; +import { ErrorState } from "../common"; + +export function QuoteCard({ + quote, + askAsset, + offerAsset, + isLoading, + error, + slippageBps, + minimumReceive, +}: { + quote?: RouteQuote; + askAsset?: RegistryAsset; + offerAsset?: RegistryAsset; + isLoading: boolean; + error?: unknown; + slippageBps: number; + minimumReceive?: string; +}) { + const priceImpact = quote + ? getPriceImpact({ + spreadAmount: quote.spread_amount, + returnAmount: quote.return_amount, + }) + : null; + const isRouterRoute = quote?.source === "router"; + const priceImpactClass = isRouterRoute + ? "status-warn" + : priceImpact?.severity === "high" || priceImpact?.severity === "extreme" + ? "status-danger" + : priceImpact?.severity === "warning" + ? "status-warn" + : "status-ok"; + const route = quote?.route; + const rateLabel = + quote && offerAsset && askAsset + ? `1 ${offerAsset.symbol} = ${( + Number( + formatAmount(quote.return_amount, askAsset.decimals).replace( + /,/g, + "" + ) + ) / + Number( + formatAmount(quote.offer_amount, offerAsset.decimals).replace( + /,/g, + "" + ) || "1" + ) + ).toLocaleString(undefined, { maximumSignificantDigits: 6 })} ${ + askAsset.symbol + }` + : "—"; + const maxSlippageLabel = formatBpsPercent(slippageBps); + + return ( +
+ {isLoading ? "Updating quote" : ""} + {error ? ( + + ) : null} + {!quote && !error ? ( +
{isLoading ? "Finding the best available route…" : "Enter an amount to preview rate, route, impact, and minimum received."}
+ ) : null} + {quote && askAsset && route ? ( + <> +
+ + {rateLabel} + +
+ {minimumReceive ? ( +
+
Minimum received
+
{formatAmount(minimumReceive, askAsset.decimals)} {askAsset.symbol}
+
+ ) : null} +
+
Price impact
+
+ {isRouterRoute + ? "Unavailable" + : priceImpact + ? formatBpsPercent(priceImpact.bps) + : "—"} +
+
+
+
Max slippage
+
{maxSlippageLabel}
+
+
+
Route
+
+ {routeSymbols(route)} · {route.hops.length} hop + {route.hops.length === 1 ? "" : "s"} +
+
+
+
+ + ) : null} +
+ ); +} diff --git a/frontend/src/components/swap/SwapForm.test.tsx b/frontend/src/components/swap/SwapForm.test.tsx new file mode 100644 index 000000000..5d37abaa0 --- /dev/null +++ b/frontend/src/components/swap/SwapForm.test.tsx @@ -0,0 +1,366 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { SwapForm } from "./SwapForm"; + +const mocks = vi.hoisted(() => ({ + wallet: { + wallet: { status: "connected", address: "juno1wallet", signer: vi.fn() } as { + status: "idle" | "connected"; + address?: string; + signer?: unknown; + }, + connect: vi.fn(), + }, + network: { + network: { + expectedChainId: "juno-1" as const, + connectedChainId: "juno-1", + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }, + switchToJuno: vi.fn(), + }, + balances: [{ denom: "ujuno", amount: "2000000" }], + balancesLoading: false, + routeReserves: {} as Record; total_share: string }>, + quote: {} as any, + refreshQuote: vi.fn(), + mutate: vi.fn(), +})); + +vi.mock("../../wallet/WalletContext", () => ({ + useWallet: () => mocks.wallet, + useNetworkGuard: () => mocks.network, +})); + +vi.mock("../../queries/useWalletBalances", () => ({ + useWalletBalances: () => ({ data: mocks.balancesLoading ? undefined : mocks.balances, isError: false, isFetching: mocks.balancesLoading }), + getWalletBalanceAmount: (balances: typeof mocks.balances | undefined, denom: string) => balances?.find((balance) => balance.denom === denom)?.amount, +})); + +vi.mock("../../queries/useSwapQuote", () => ({ + useSwapQuote: () => mocks.quote, +})); + +vi.mock("../../queries/usePools", () => ({ + useRouteReserves: () => mocks.routeReserves, + usePoolCandles: () => ({ + data: [], + access: { source: "indexer", isFallback: false, isMock: false, isStale: false }, + isLoading: false, + isFetching: false, + refetch: vi.fn(), + }), +})); + +vi.mock("../../settings/SlippageSettingsContext", () => ({ + useSlippageSettings: () => ({ slippageBps: 50, formattedSlippagePercent: "0.5", maxSpread: "0.005" }), +})); + +vi.mock("../../mutations/useSwapTx", () => ({ + buildSwapExecuteInstruction: () => ({ contractAddress: "juno1pair", msg: {} }), + useSwapTx: () => ({ mutate: mocks.mutate, isPending: false, isError: false, isSuccess: false, txState: { status: "idle", label: "Ready" } }), +})); + +const pool: RegistryPool = { + id: "test", + label: "JUNO / TEST", + pair: "juno1pair", + lpToken: "factory/juno1pair/lp", + type: "xyk", + feeBps: 30, + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, verified: true }, + { kind: "ibc", id: "ibc/test", symbol: "TEST", decimals: 6, verified: true }, + ], + explorer: "https://ping.pub/juno/address/juno1pair", + enabled: true, + status: "active", + verified: true, + source: "registry", +}; + +const atomPool: RegistryPool = { + ...pool, + id: "atom", + label: "TEST / ATOM", + pair: "juno1pairatom", + lpToken: "factory/juno1pairatom/lp", + assets: [ + { kind: "ibc", id: "ibc/test", symbol: "TEST", decimals: 6, verified: true }, + { kind: "ibc", id: "ibc/atom", symbol: "ATOM", decimals: 6, verified: true }, + ], +}; + +function directRoute() { + return { + id: "direct", + hops: [{ pool, offerAsset: pool.assets[0], askAsset: pool.assets[1] }], + operations: [{ astro_swap: { offer_asset_info: { native_token: { denom: "ujuno" } }, ask_asset_info: { native_token: { denom: "ibc/test" } } } }], + }; +} + +function routerRoute() { + return { + id: "router", + hops: [ + { pool, offerAsset: pool.assets[0], askAsset: pool.assets[1] }, + { pool: atomPool, offerAsset: atomPool.assets[0], askAsset: atomPool.assets[1] }, + ], + operations: [ + { astro_swap: { offer_asset_info: { native_token: { denom: "ujuno" } }, ask_asset_info: { native_token: { denom: "ibc/test" } } } }, + { astro_swap: { offer_asset_info: { native_token: { denom: "ibc/test" } }, ask_asset_info: { native_token: { denom: "ibc/atom" } } } }, + ], + }; +} + +function enterOneJuno() { + fireEvent.change(screen.getByRole("textbox", { name: /you send amount/i }), { target: { value: "1" } }); +} + +describe("SwapForm", () => { + beforeEach(() => { + mocks.mutate.mockReset(); + mocks.wallet.connect.mockReset(); + mocks.network.switchToJuno.mockReset(); + mocks.balancesLoading = false; + mocks.wallet.wallet = { status: "connected", address: "juno1wallet", signer: vi.fn() }; + mocks.network.network = { + expectedChainId: "juno-1", + connectedChainId: "juno-1", + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }; + mocks.balances = [{ denom: "ujuno", amount: "2000000" }]; + mocks.routeReserves = {}; + mocks.quote = { + data: { offer_amount: "1000000", return_amount: "990000", spread_amount: "1000", commission_amount: "3000", source: "pair", route: directRoute() }, + isSuccess: true, + isFetching: false, + isError: false, + error: null, + isDebouncing: false, + isExpired: false, + quoteUpdatedAt: 1_000, + expiresInMs: 20_000, + refreshQuote: mocks.refreshQuote, + }; + mocks.refreshQuote.mockReset(); + mocks.refreshQuote.mockImplementation(async () => ({ data: mocks.quote.data, dataUpdatedAt: 1_000, isError: false })); + }); + + it("reviews and submits a direct pair swap with the bounded amount", async () => { + render(); + enterOneJuno(); + + const button = screen.getByRole("button", { name: /review swap/i }); + expect(button.hasAttribute("disabled")).toBe(false); + + fireEvent.click(button); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + expect(mocks.mutate).toHaveBeenCalledWith({ + pool, + route: directRoute(), + offerAsset: expect.objectContaining(pool.assets[0]), + askAsset: expect.objectContaining(pool.assets[1]), + amount: "1000000", + maxSpread: "0.005", + minimumReceive: "985050", + source: "pair", + }); + }); + + it("requires router impact acknowledgement before review and submission", async () => { + mocks.quote.data = { offer_amount: "1000000", return_amount: "970000", spread_amount: "0", commission_amount: "0", source: "router", route: routerRoute() }; + render(); + enterOneJuno(); + + expect(screen.getByRole("button", { name: /acknowledge unavailable price impact/i }).hasAttribute("disabled")).toBe(true); + fireEvent.click(screen.getByLabelText(/price impact is unavailable for this multi-hop route/i)); + fireEvent.click(screen.getByRole("button", { name: /review swap/i })); + fireEvent.click(await screen.findByRole("button", { name: /confirm in wallet/i })); + + expect(mocks.mutate).toHaveBeenCalledWith(expect.objectContaining({ + route: routerRoute(), + source: "router", + minimumReceive: "965150", + })); + expect(screen.getByText(/price impact is unavailable for this multi-hop route/i)).toBeTruthy(); + }); + + it("connects from the primary action without discarding intent", () => { + mocks.wallet.wallet = { status: "idle" }; + render(); + const button = screen.getByRole("button", { name: /connect wallet to swap/i }); + expect(button.hasAttribute("disabled")).toBe(false); + fireEvent.click(button); + expect(mocks.wallet.connect).toHaveBeenCalledOnce(); + }); + + it("switches network from the primary action", () => { + mocks.network.network = { ...mocks.network.network, connectedChainId: "osmosis-1", isWrongNetwork: true, isJunoReady: false }; + render(); + const button = screen.getByRole("button", { name: /switch to juno to swap/i }); + expect(button.hasAttribute("disabled")).toBe(false); + fireEvent.click(button); + expect(mocks.network.switchToJuno).toHaveBeenCalledOnce(); + }); + + it("disables swap for insufficient balance", () => { + mocks.balances = [{ denom: "ujuno", amount: "999999" }]; + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /insufficient juno balance/i }).hasAttribute("disabled")).toBe(true); + }); + + it("blocks execution while the offer balance is unknown", () => { + mocks.balancesLoading = true; + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /loading wallet balance/i }).hasAttribute("disabled")).toBe(true); + }); + + it("disables swap while the current route preview is unavailable", () => { + mocks.quote = { ...mocks.quote, data: undefined, isSuccess: false, isFetching: false, isError: true, error: new Error("quote failed") }; + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /route preview unavailable/i }).hasAttribute("disabled")).toBe(true); + }); + + it("requires explicit confirmation for high-impact direct quotes", () => { + mocks.quote = { + ...mocks.quote, + data: { offer_amount: "1000000", return_amount: "900000", spread_amount: "100000", commission_amount: "3000", source: "pair", route: directRoute() }, + }; + + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /acknowledge high price impact/i }).hasAttribute("disabled")).toBe(true); + fireEvent.click(screen.getByLabelText(/i understand this quote has high price impact/i)); + expect(screen.getByRole("button", { name: /review swap/i }).hasAttribute("disabled")).toBe(false); + }); + + it("hard-blocks extreme price impact", () => { + mocks.quote = { + ...mocks.quote, + data: { offer_amount: "1000000", return_amount: "500000", spread_amount: "500000", commission_amount: "3000", source: "pair", route: directRoute() }, + }; + + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /price impact too high/i }).hasAttribute("disabled")).toBe(true); + expect(screen.getByRole("alert").textContent).toMatch(/exceeds the 15% safety limit/i); + }); + + it("blocks unverified routes until the user acknowledges risk", () => { + const unverifiedPool: RegistryPool = { ...pool, source: "factory", verified: false }; + mocks.quote.data = { + offer_amount: "1000000", + return_amount: "990000", + spread_amount: "1000", + commission_amount: "3000", + source: "pair", + route: { + id: "unverified-direct", + hops: [{ pool: unverifiedPool, offerAsset: unverifiedPool.assets[0], askAsset: unverifiedPool.assets[1] }], + operations: [], + }, + }; + + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /acknowledge unverified route/i }).hasAttribute("disabled")).toBe(true); + fireEvent.click(screen.getByLabelText(/i understand this swap route uses unverified or risky assets/i)); + expect(screen.getByRole("button", { name: /review swap/i }).hasAttribute("disabled")).toBe(false); + }); + + it("blocks an expired quote", () => { + mocks.quote = { ...mocks.quote, isExpired: true, expiresInMs: 0 }; + render(); + enterOneJuno(); + expect(screen.getByRole("button", { name: /quote expired/i }).hasAttribute("disabled")).toBe(true); + expect(screen.getAllByText(/expired — refresh required/i).length).toBeGreaterThan(0); + }); + + it("describes reverse simulation as a target, not guaranteed exact output", () => { + render(); + const receiveInput = screen.getByRole("textbox", { name: /you receive amount/i }); + fireEvent.change(receiveInput, { target: { value: "2" } }); + // Exact-out is signalled once, by the input label, not by a separate notice. + expect(screen.getByRole("textbox", { name: /target receive amount/i })).toBeTruthy(); + expect(screen.queryByText(/swap exact output/i)).toBeNull(); + }); + + it("invalidates review when the reviewed quote version changes", async () => { + const view = render(); + enterOneJuno(); + fireEvent.click(screen.getByRole("button", { name: /review swap/i })); + expect(await screen.findByRole("button", { name: /confirm in wallet/i })).toBeTruthy(); + + mocks.quote = { ...mocks.quote, quoteUpdatedAt: 2_000 }; + view.rerender(); + expect(screen.getByRole("button", { name: /confirm in wallet/i }).hasAttribute("disabled")).toBe(true); + expect(screen.getByRole("alert").textContent).toMatch(/quote version changed/i); + }); + + it("closes review when route selection changes", async () => { + const view = render(); + enterOneJuno(); + fireEvent.click(screen.getByRole("button", { name: /review swap/i })); + expect(await screen.findByRole("button", { name: /confirm in wallet/i })).toBeTruthy(); + + mocks.quote = { + ...mocks.quote, + data: { ...mocks.quote.data, route: { ...directRoute(), id: "changed-route" } }, + }; + view.rerender(); + expect(screen.queryByRole("button", { name: /confirm in wallet/i })).toBeNull(); + expect(mocks.mutate).not.toHaveBeenCalled(); + }); + + it("starts without transaction intent and reserves JUNO for gas on MAX", () => { + render(); + const input = screen.getByRole("textbox", { name: /you send amount/i }) as HTMLInputElement; + expect(input.value).toBe(""); + expect(screen.getAllByText(/verified · juno native/i).length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole("button", { name: /max/i })); + expect(input.value).toBe("1.75"); + expect(screen.getByText(/reserves 0.25 juno for network fees/i)).toBeTruthy(); + }); + + it("keeps the last quote visible but subdued while a replacement loads", () => { + mocks.quote = { ...mocks.quote, isFetching: true }; + render(); + + const quote = document.querySelector("section.quote-card"); + expect(quote?.className).toContain("quote-card-updating"); + expect(quote?.getAttribute("aria-busy")).toBe("true"); + expect(screen.getByText(/^1 JUNO = /)).toBeTruthy(); + }); + + it("feeds live route reserves into visible liquidity risk", () => { + mocks.routeReserves = { + [pool.pair]: { assets: [{ amount: "999999" }, { amount: "1000000" }], total_share: "1000000" }, + }; + render(); + + expect(screen.getByText("Thin liquidity")).toBeTruthy(); + }); + + it("closes settings with Escape and returns focus to the trigger", () => { + render(); + const trigger = screen.getByRole("button", { name: /slippage 0.5%/i }); + trigger.focus(); + fireEvent.click(trigger); + expect(screen.getByRole("dialog", { name: /dex settings/i })).toBeTruthy(); + fireEvent.keyDown(document, { key: "Escape" }); + expect(screen.queryByRole("dialog", { name: /dex settings/i })).toBeNull(); + expect(document.activeElement).toBe(trigger); + }); +}); diff --git a/frontend/src/components/swap/SwapForm.tsx b/frontend/src/components/swap/SwapForm.tsx new file mode 100644 index 000000000..7f6996c85 --- /dev/null +++ b/frontend/src/components/swap/SwapForm.tsx @@ -0,0 +1,399 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Box, Button, Stack, Text } from "@interchain-ui/react"; +import { dexRegistry, type RegistryAsset, type RegistryPool } from "../../config/registry"; +import type { SwapQuoteMode } from "../../lib/astroport/queries"; +import { formatAmount, isBaseAmountGreaterThan, parseTokenAmount } from "../../lib/format/amounts"; +import { assessRouteRisk } from "../../lib/risk"; +import { HIGH_SLIPPAGE_BPS, calculateMinimumReceived, formatBpsPercent, getPriceImpact, slippageBpsToMaxSpread } from "../../lib/swap/slippage"; +import { buildSwapExecuteInstruction, useSwapTx } from "../../mutations/useSwapTx"; +import { estimateExecuteNetworkFee, type NetworkFeeEstimate } from "../../lib/cosmjs/fees"; +import { type RouteQuote, useSwapQuote } from "../../queries/useSwapQuote"; +import { useRouteReserves } from "../../queries/usePools"; +import { getWalletBalanceAmount, useWalletBalances } from "../../queries/useWalletBalances"; +import { useSlippageSettings } from "../../settings/SlippageSettingsContext"; +import { useNetworkGuard, useWallet } from "../../wallet/WalletContext"; +import { RiskAcknowledgement, RiskBadgeList, TokenAmountInput, TransactionReview } from "../common"; +import { SettingsPanel } from "../settings/SettingsPanel"; +import { QuoteCard } from "./QuoteCard"; +import { TokenSelect } from "./TokenSelect"; +import { TxStatusDialog } from "../tx/TxStatusDialog"; + +type SwapFormProps = { + pool: RegistryPool; + pools?: RegistryPool[]; + onMarketPoolChange?: (pool: RegistryPool) => void; +}; + +type SwapReviewSnapshot = { + route: RouteQuote["route"]; + source: RouteQuote["source"]; + offerAmount: string; + returnAmount: string; + commissionAmount: string; + minimumReceive: string; + slippageBps: number; + updatedAt: number; + mode: SwapQuoteMode; + networkFeeEstimate?: NetworkFeeEstimate; +}; + +function isPositiveBaseAmount(amount: string) { + return /^\d+$/.test(amount) && BigInt(amount) > 0n; +} + +const JUNO_GAS_RESERVE = 250_000n; + +function spendableBalance(asset: RegistryAsset, balance: string | undefined) { + if (!balance || !/^\d+$/.test(balance)) return "0"; + const amount = BigInt(balance); + if (asset.id !== "ujuno") return amount.toString(); + return amount > JUNO_GAS_RESERVE ? (amount - JUNO_GAS_RESERVE).toString() : "0"; +} + +function inputAmountFromBase(baseAmount: string, decimals: number) { + return formatAmount(baseAmount, decimals, decimals).replace(/,/g, ""); +} + +function buildSelectableAssets(pools: RegistryPool[]) { + const byId = new Map(); + for (const candidatePool of pools) { + for (const asset of candidatePool.assets) { + const existing = byId.get(asset.id); + byId.set(asset.id, { + ...existing, + ...asset, + logoURI: existing?.logoURI ?? asset.logoURI, + verified: existing?.verified === true || asset.verified === true, + poolCount: (existing?.poolCount ?? 0) + 1, + }); + } + } + return Array.from(byId.values()).sort((a, b) => a.symbol.localeCompare(b.symbol)); +} + +export function SwapForm({ pool, pools, onMarketPoolChange }: SwapFormProps) { + const allPools = useMemo(() => pools && pools.length > 0 ? pools : [pool], [pool, pools]); + const selectableAssets = useMemo(() => buildSelectableAssets(allPools), [allPools]); + const { wallet, connect } = useWallet(); + const { network, switchToJuno } = useNetworkGuard(); + const [offerId, setOfferId] = useState(pool.assets[0].id); + const [askId, setAskId] = useState(pool.assets[1].id); + const [amount, setAmount] = useState(""); + const [askAmount, setAskAmount] = useState(""); + const [quoteMode, setQuoteMode] = useState("exact-in"); + const [riskAcknowledged, setRiskAcknowledged] = useState(false); + const [priceImpactAcknowledged, setPriceImpactAcknowledged] = useState(false); + const [slippageAcknowledged, setSlippageAcknowledged] = useState(false); + const [unavailableImpactAcknowledged, setUnavailableImpactAcknowledged] = useState(false); + const [reviewSnapshot, setReviewSnapshot] = useState(); + const [isPreparingReview, setIsPreparingReview] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const closeSettings = useCallback(() => setSettingsOpen(false), []); + const { slippageBps, formattedSlippagePercent } = useSlippageSettings(); + const offerAsset = selectableAssets.find((asset) => asset.id === offerId) ?? pool.assets[0]; + const askAsset = selectableAssets.find((asset) => asset.id === askId && asset.id !== offerAsset.id) ?? selectableAssets.find((asset) => asset.id !== offerAsset.id) ?? pool.assets[1]; + const parsedOfferInput = parseTokenAmount(amount, offerAsset.decimals); + const parsedAskInput = parseTokenAmount(askAmount, askAsset.decimals); + const quoteInputBaseAmount = quoteMode === "exact-out" ? parsedAskInput.baseAmount : parsedOfferInput.baseAmount; + const activeParsedAmount = quoteMode === "exact-out" ? parsedAskInput : parsedOfferInput; + const walletAddress = wallet.status === "connected" ? wallet.address : undefined; + const balances = useWalletBalances(walletAddress, allPools); + const offerBalance = getWalletBalanceAmount(balances.data, offerAsset.id); + const askBalance = getWalletBalanceAmount(balances.data, askAsset.id); + const balancesReady = wallet.status !== "connected" || (balances.data !== undefined && !balances.isFetching); + const quote = useSwapQuote(allPools, offerAsset, askAsset, quoteInputBaseAmount, quoteMode); + const signerOrClient = wallet.status === "connected" ? wallet.signer : undefined; + const swapTx = useSwapTx(signerOrClient, walletAddress); + const requiredOfferBaseAmount = quoteMode === "exact-out" && quote.data ? quote.data.offer_amount : parsedOfferInput.baseAmount; + const hasAmount = activeParsedAmount.isValid && isPositiveBaseAmount(quoteInputBaseAmount); + const sameToken = offerAsset.id === askAsset.id; + const exceedsBalance = Boolean(balancesReady && isPositiveBaseAmount(requiredOfferBaseAmount) && isBaseAmountGreaterThan(requiredOfferBaseAmount, offerBalance ?? "0")); + const quoteReady = quote.isSuccess && Boolean(quote.data) && !quote.isFetching && !quote.isError && !quote.isDebouncing && !quote.isExpired; + const priceImpact = quote.data && quote.data.source === "pair" ? getPriceImpact({ spreadAmount: quote.data.spread_amount, returnAmount: quote.data.return_amount }) : null; + const hasHighPriceImpact = priceImpact?.severity === "high"; + const hasExtremePriceImpact = priceImpact?.severity === "extreme"; + const hasUnavailablePriceImpact = quote.data?.source === "router"; + const hasHighSlippage = slippageBps > HIGH_SLIPPAGE_BPS; + const selectedRoute = quote.data?.route; + const routeReserves = useRouteReserves(selectedRoute); + useEffect(() => { + onMarketPoolChange?.(selectedRoute?.hops[0]?.pool ?? pool); + }, [onMarketPoolChange, pool, selectedRoute]); + const routeRisk = assessRouteRisk(selectedRoute, routeReserves); + const minimumReceive = quote.data ? calculateMinimumReceived(quote.data.return_amount, slippageBps) : "0"; + useEffect(() => { + setRiskAcknowledged(false); + setPriceImpactAcknowledged(false); + setUnavailableImpactAcknowledged(false); + setReviewSnapshot(undefined); + }, [quoteInputBaseAmount, quoteMode, offerAsset.id, askAsset.id, selectedRoute?.id]); + useEffect(() => { + setSlippageAcknowledged(false); + setReviewSnapshot(undefined); + }, [slippageBps]); + + const validationError = !activeParsedAmount.isValid + ? activeParsedAmount.error + : sameToken + ? "Choose two different tokens" + : !hasAmount + ? "Enter amount" + : !balancesReady + ? "Loading wallet balance…" + : quote.isDebouncing + ? "Updating quote…" + : exceedsBalance + ? `Insufficient ${offerAsset.symbol} balance` + : quote.isError + ? "Route preview unavailable" + : quote.isExpired + ? "Quote expired — refresh required" + : quote.isFetching || (hasAmount && !quoteReady) + ? "Refreshing route…" + : !selectedRoute + ? "No route found" + : routeRisk.blocked + ? "Blocked asset or pool" + : hasExtremePriceImpact + ? `Price impact too high (${formatBpsPercent(priceImpact.bps)})` + : hasHighPriceImpact && !priceImpactAcknowledged + ? "Acknowledge high price impact" + : hasUnavailablePriceImpact && !unavailableImpactAcknowledged + ? "Acknowledge unavailable price impact" + : hasHighSlippage && !slippageAcknowledged + ? "Acknowledge high slippage" + : routeRisk.requiresAcknowledgement && !riskAcknowledged + ? "Acknowledge unverified route" + : undefined; + const submitDisabled = wallet.status === "connected" && (!network.isJunoReady + || network.isWrongNetwork + || Boolean(validationError) + || swapTx.isPending + || isPreparingReview); + const primaryActionDisabled = wallet.status === "connecting" + || network.isRecovering + || (wallet.status === "connected" && network.isJunoReady && !network.isWrongNetwork && (Boolean(validationError) || swapTx.isPending || isPreparingReview)); + const actionCopy = network.isWrongNetwork + ? "Switch to Juno to swap" + : wallet.status === "connected" && !network.isJunoReady + ? "Juno network required" + : wallet.status !== "connected" + ? "Connect wallet to swap" + : swapTx.isPending + ? "Swapping…" + : isPreparingReview + ? "Refreshing quote…" + : validationError ?? "Review swap"; + + const updateOfferAmount = (nextAmount: string) => { + setAmount(nextAmount); + setQuoteMode("exact-in"); + }; + + const updateAskAmount = (nextAmount: string) => { + setAskAmount(nextAmount); + setQuoteMode("exact-out"); + }; + + const handleOfferChange = (next: string) => { + setOfferId(next); + if (next === askId) setAskId(selectableAssets.find((asset) => asset.id !== next)?.id ?? askId); + setQuoteMode("exact-in"); + }; + + const handleFlip = () => { + const nextOfferAmount = quote.data?.return_amount ? formatAmount(quote.data.return_amount, askAsset.decimals) : askAmount; + setOfferId(askAsset.id); + setAskId(offerAsset.id); + setAmount(nextOfferAmount || ""); + setAskAmount(""); + setQuoteMode("exact-in"); + }; + + const handleReview = async () => { + if (submitDisabled || !selectedRoute || !quote.data) return; + setIsPreparingReview(true); + const refreshed = await quote.refreshQuote(); + setIsPreparingReview(false); + if (!refreshed.data || refreshed.isError) return; + const refreshedMinimum = calculateMinimumReceived(refreshed.data.return_amount, slippageBps); + const instruction = buildSwapExecuteInstruction({ + pool: refreshed.data.route.hops[0]?.pool, + route: refreshed.data.route, + offerAsset, + askAsset, + amount: refreshed.data.offer_amount, + maxSpread: slippageBpsToMaxSpread(slippageBps), + minimumReceive: refreshedMinimum, + source: refreshed.data.source, + }); + const networkFeeEstimate = await estimateExecuteNetworkFee(signerOrClient, walletAddress, [instruction]).catch(() => undefined); + setReviewSnapshot({ + route: refreshed.data.route, + source: refreshed.data.source, + offerAmount: refreshed.data.offer_amount, + returnAmount: refreshed.data.return_amount, + commissionAmount: refreshed.data.commission_amount, + minimumReceive: refreshedMinimum, + slippageBps, + updatedAt: refreshed.dataUpdatedAt, + mode: quoteMode, + networkFeeEstimate, + }); + }; + + const handlePrimaryAction = async () => { + if (wallet.status !== "connected") { + await connect(); + return; + } + if (network.isWrongNetwork || !network.isJunoReady) { + await switchToJuno(); + return; + } + await handleReview(); + }; + + const reviewIsCurrent = Boolean(reviewSnapshot + && quote.data + && !quote.isExpired + && quote.data.route.id === reviewSnapshot.route.id + && quote.data.offer_amount === reviewSnapshot.offerAmount + && quote.data.return_amount === reviewSnapshot.returnAmount + && quote.quoteUpdatedAt === reviewSnapshot.updatedAt + && slippageBps === reviewSnapshot.slippageBps); + + const handleSwap = () => { + if (!reviewSnapshot || !reviewIsCurrent || swapTx.isPending) return; + swapTx.mutate({ + pool: reviewSnapshot.route.hops[0]?.pool, + route: reviewSnapshot.route, + offerAsset, + askAsset, + amount: reviewSnapshot.offerAmount, + maxSpread: slippageBpsToMaxSpread(reviewSnapshot.slippageBps), + minimumReceive: reviewSnapshot.minimumReceive, + source: reviewSnapshot.source, + }); + setReviewSnapshot(undefined); + }; + + return ( + + + Swap +
+ + {settingsOpen ? : null} +
+
+
+ + + updateOfferAmount(inputAmountFromBase(spendableBalance(offerAsset, offerBalance), offerAsset.decimals))} + onHalf={() => updateOfferAmount(inputAmountFromBase((BigInt(spendableBalance(offerAsset, offerBalance)) / 2n).toString(), offerAsset.decimals))} + fiatHint={quoteMode === "exact-out" && quote.data ? Estimated input for target : offerAsset.id === "ujuno" ? MAX reserves 0.25 JUNO for network fees : undefined} + showQuickActions + showTokenIdentity={false} + /> + + + + + + + + asset.id !== offerAsset.id)} value={askAsset.id} onChange={(next) => { setAskId(next); setQuoteMode("exact-in"); }} label="To asset" balances={balances.data} showIdentifier={false} hideLabel /> + + +
+ + {hasHighPriceImpact ? ( + + ) : null} + {hasExtremePriceImpact ?
This swap is blocked because its {formatBpsPercent(priceImpact.bps)} price impact exceeds the 15% safety limit. Reduce the amount or choose another route.
: null} + {hasUnavailablePriceImpact ? ( + + ) : null} + {hasHighSlippage ? ( + + ) : null} + {selectedRoute ? : null} + + {network.isWrongNetwork ? Transactions are blocked while your wallet is off Juno mainnet. : null} + + + HIGH_SLIPPAGE_BPS ? "warning" as const : "default" as const }, + { label: "Price impact · estimated", value: reviewSnapshot.source === "pair" && priceImpact ? formatBpsPercent(priceImpact.bps) : "Unavailable for multi-hop route", tone: reviewSnapshot.source === "router" ? "warning" as const : priceImpact?.severity === "high" || priceImpact?.severity === "extreme" ? "danger" as const : "default" as const }, + { label: "Pool commission · estimated", value: reviewSnapshot.source === "pair" ? `${formatAmount(reviewSnapshot.commissionAmount, askAsset.decimals)} ${askAsset.symbol}` : "Unavailable for multi-hop route", tone: reviewSnapshot.source === "router" ? "warning" as const : "default" as const }, + { label: "Route", value: `${reviewSnapshot.route.hops.length} hop${reviewSnapshot.route.hops.length === 1 ? "" : "s"}` }, + { label: "Assets", value: `${offerAsset.symbol} (${offerAsset.verified === true ? "verified" : "unverified"}, ${offerAsset.kind}) → ${askAsset.symbol} (${askAsset.verified === true ? "verified" : "unverified"}, ${askAsset.kind})` }, + { label: "Pool status", value: reviewSnapshot.route.hops.map((hop) => `${hop.pool.label}: ${hop.pool.status}${hop.pool.verified === true ? ", verified" : ", unverified"}`).join(" · ") }, + ] : []} + disclosures={reviewSnapshot ? [ + { label: "Offer denom / contract", value: offerAsset.id }, + { label: "Receive denom / contract", value: askAsset.id }, + ...reviewSnapshot.route.hops.map((hop, index) => ({ label: `Pair contract ${index + 1}`, value: hop.pool.pair })), + ...(reviewSnapshot.source === "router" ? [{ label: "Router contract", value: dexRegistry.router }] : []), + ] : []} + warning={!reviewIsCurrent && reviewSnapshot ? "The amount, route, slippage, or quote version changed. Close this review and refresh it before signing." : undefined} + confirmDisabled={!reviewIsCurrent} + pending={swapTx.isPending} + onClose={() => setReviewSnapshot(undefined)} + onConfirm={handleSwap} + /> +
+ ); +} diff --git a/frontend/src/components/swap/SwapPage.tsx b/frontend/src/components/swap/SwapPage.tsx new file mode 100644 index 000000000..b3956b1f8 --- /dev/null +++ b/frontend/src/components/swap/SwapPage.tsx @@ -0,0 +1,31 @@ +import { Box, Stack } from "@interchain-ui/react"; +import { useCallback, useState } from "react"; +import { useDexRegistry } from "../../queries/useDexRegistry"; +import type { RegistryPool } from "../../config/registry"; +import { EmptyState, ErrorState, OptionalDataState, Skeleton } from "../common"; +import { SwapForm } from "./SwapForm"; +import { PriceCandleChart } from "../charts/PriceCandleChart"; + +export function SwapPage() { + const { pools, discovery } = useDexRegistry(); + const pool = pools[0]; + const [marketPair, setMarketPair] = useState(); + const marketPool = pools.find((candidate) => candidate.pair === marketPair) ?? pool; + const handleMarketPoolChange = useCallback((nextPool: RegistryPool) => setMarketPair(nextPool.pair), []); + + return ( + + + {discovery.isFetching && !pool ?
: null} + {discovery.isError && pool ? void discovery.refetch()}>The selected reviewed market remains available. : null} + {discovery.isError && !pool ? void discovery.refetch()} /> : null} + {pool ? : Add a real Juno pair to the strict registry before exposing swaps.} +
+ {marketPool ? ( + + + + ) : null} +
+ ); +} diff --git a/frontend/src/components/swap/TokenSelect.test.tsx b/frontend/src/components/swap/TokenSelect.test.tsx new file mode 100644 index 000000000..78590970f --- /dev/null +++ b/frontend/src/components/swap/TokenSelect.test.tsx @@ -0,0 +1,88 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { WalletBalance } from "../../queries/useWalletBalances"; +import { matchesTokenSearch, TokenSelect, type TokenSelectorAsset } from "./TokenSelect"; + +const assets: TokenSelectorAsset[] = [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, logoURI: "https://example.com/juno.svg", verified: true }, + { kind: "ibc", id: "ibc/atomhash", symbol: "ATOM", decimals: 6, denomTrace: "transfer/channel-1/uatom", verified: true }, + { kind: "cw20", id: "juno1tokencontract", symbol: "RAW", decimals: 6, verified: false }, +]; + +const balances: WalletBalance[] = [ + { denom: "ujuno", symbol: "JUNO", decimals: 6, amount: "1234567", source: "registry", isKnownDenom: true }, + { denom: "ibc/atomhash", symbol: "ATOM", decimals: 6, amount: "2500000", source: "registry", isKnownDenom: true }, +]; + +describe("TokenSelect", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + it("filters tokens by symbol, denom trace, and address", () => { + expect(matchesTokenSearch(assets[1], "atom")).toBe(true); + expect(matchesTokenSearch(assets[1], "channel-1")).toBe(true); + expect(matchesTokenSearch(assets[2], "tokencontract")).toBe(true); + expect(matchesTokenSearch(assets[0], "osmosis")).toBe(false); + }); + + it("searches the modal result list", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /juno/i })); + fireEvent.change(screen.getByLabelText(/search tokens/i), { target: { value: "atom" } }); + + expect(screen.getAllByRole("button", { name: /atom/i }).length).toBeGreaterThan(0); + expect(screen.queryByRole("button", { name: /raw/i })).toBeNull(); + }); + + it("persists favorites to localStorage and ranks them first when reopened", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /juno/i })); + fireEvent.click(screen.getByLabelText(/add atom favorite/i)); + expect(JSON.parse(window.localStorage.getItem("juno-dex.token-selector.favorites") ?? "[]")).toEqual(["ibc/atomhash"]); + fireEvent.click(screen.getByLabelText(/close modal/i)); + + fireEvent.click(screen.getByRole("button", { name: /juno/i })); + const rows = screen.getAllByRole("listitem"); + expect(within(rows[0]).getAllByRole("button", { name: /atom/i }).length).toBeGreaterThan(0); + }); + + it("focuses the search field, closes with Escape, and returns focus to the trigger", () => { + render(); + + const trigger = screen.getByRole("button", { name: /asset: juno/i }); + trigger.focus(); + fireEvent.click(trigger); + + expect(screen.getByRole("dialog", { name: /select asset token/i })).toBeTruthy(); + const search = screen.getByLabelText(/search tokens/i); + expect(document.activeElement).toBe(search); + + fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" }); + expect(screen.queryByRole("dialog")).toBeNull(); + expect(document.activeElement).toBe(trigger); + }); + + it("displays formatted wallet balances and unverified risk badges", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /juno/i })); + + expect(screen.getByText("1.234567")).toBeTruthy(); + expect(screen.getByText("2.5")).toBeTruthy(); + expect(screen.getAllByText(/unverified/i).length).toBeGreaterThan(0); + }); + + it("offers a custom asset action for unknown search terms when enabled", () => { + const onCreateCustomAsset = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: /juno/i })); + fireEvent.change(screen.getByLabelText(/search tokens/i), { target: { value: "factory/juno1issuer/custom" } }); + fireEvent.click(screen.getByRole("button", { name: /use unverified asset/i })); + + expect(onCreateCustomAsset).toHaveBeenCalledWith("factory/juno1issuer/custom"); + }); +}); diff --git a/frontend/src/components/swap/TokenSelect.tsx b/frontend/src/components/swap/TokenSelect.tsx new file mode 100644 index 000000000..43ea35616 --- /dev/null +++ b/frontend/src/components/swap/TokenSelect.tsx @@ -0,0 +1,163 @@ +import { useEffect, useId, useMemo, useRef, useState } from "react"; +import type { RegistryAsset } from "../../config/registry"; +import { formatAmount } from "../../lib/format/amounts"; +import { assessAssetRisk } from "../../lib/risk"; +import type { WalletBalance } from "../../queries/useWalletBalances"; +import { Modal, RiskBadgeList, TokenLogo } from "../common"; + +const FAVORITES_STORAGE_KEY = "juno-dex.token-selector.favorites"; +const RECENTS_STORAGE_KEY = "juno-dex.token-selector.recents"; +const MAX_RECENTS = 5; + +export type TokenSelectorAsset = RegistryAsset & { + name?: string; + verified?: boolean; + poolCount?: number; +}; + +type TokenSelectorProps = { + assets: TokenSelectorAsset[]; + value: string; + onChange: (value: string) => void; + label: string; + balances?: readonly WalletBalance[]; + disabledIds?: string[]; + showIdentifier?: boolean; + hideLabel?: boolean; + onCreateCustomAsset?: (query: string) => void; +}; + +function readStoredIds(key: string): string[] { + if (typeof window === "undefined") return []; + try { + const parsed = JSON.parse(window.localStorage.getItem(key) ?? "[]"); + return Array.isArray(parsed) ? parsed.filter((value): value is string => typeof value === "string") : []; + } catch { + return []; + } +} + +function writeStoredIds(key: string, ids: string[]) { + if (typeof window === "undefined") return; + window.localStorage.setItem(key, JSON.stringify(ids)); +} + +export function matchesTokenSearch(asset: TokenSelectorAsset, query: string): boolean { + const normalized = query.trim().toLowerCase(); + if (!normalized) return true; + return [asset.symbol, asset.name, asset.id, asset.denomTrace].filter(Boolean).some((value) => String(value).toLowerCase().includes(normalized)); +} + +function assetBalance(asset: TokenSelectorAsset, balances?: readonly WalletBalance[]) { + return balances?.find((balance) => balance.denom === asset.id)?.amount; +} + +function assetOriginLabel(asset: TokenSelectorAsset) { + if (asset.kind === "ibc") return "IBC asset"; + if (asset.kind === "cw20") return "CW20 token"; + return "Juno native"; +} + +export function TokenSelect({ assets, value, onChange, label, balances, disabledIds = [], showIdentifier = true, hideLabel = false, onCreateCustomAsset }: TokenSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const [query, setQuery] = useState(""); + const [favorites, setFavorites] = useState(() => readStoredIds(FAVORITES_STORAGE_KEY)); + const [recents, setRecents] = useState(() => readStoredIds(RECENTS_STORAGE_KEY)); + const searchRef = useRef(null); + const helpId = useId(); + const resultsId = useId(); + const selected = assets.find((asset) => asset.id === value) ?? assets[0]; + const selectedAssessment = selected ? assessAssetRisk(selected) : undefined; + const disabled = new Set(disabledIds); + + useEffect(() => { + if (isOpen) searchRef.current?.focus(); + }, [isOpen]); + + const trimmedQuery = query.trim(); + const visibleAssets = useMemo(() => { + const favoriteRank = new Map(favorites.map((id, index) => [id, index])); + const recentRank = new Map(recents.map((id, index) => [id, index])); + return assets + .filter((asset) => matchesTokenSearch(asset, query)) + .sort((a, b) => { + const favDelta = (favoriteRank.get(a.id) ?? 999) - (favoriteRank.get(b.id) ?? 999); + if (favDelta !== 0) return favDelta; + const recentDelta = (recentRank.get(a.id) ?? 999) - (recentRank.get(b.id) ?? 999); + if (recentDelta !== 0) return recentDelta; + return a.symbol.localeCompare(b.symbol); + }); + }, [assets, favorites, query, recents]); + const canCreateCustomAsset = Boolean(onCreateCustomAsset && trimmedQuery); + + const persistFavorite = (id: string) => { + const next = favorites.includes(id) ? favorites.filter((favorite) => favorite !== id) : [id, ...favorites]; + setFavorites(next); + writeStoredIds(FAVORITES_STORAGE_KEY, next); + }; + + const selectAsset = (id: string) => { + if (disabled.has(id)) return; + const nextRecents = [id, ...recents.filter((recent) => recent !== id)].slice(0, MAX_RECENTS); + setRecents(nextRecents); + writeStoredIds(RECENTS_STORAGE_KEY, nextRecents); + onChange(id); + setQuery(""); + setIsOpen(false); + }; + + return ( +
+ {hideLabel ? null : {label}} + + setIsOpen(false)}> +
+ setQuery(event.target.value)} /> +
Favorites are saved on this device. Assets that have not been reviewed are marked unverified.
+
+ {visibleAssets.length === 0 ? ( +
+

No tokens match “{query}”.

+ {canCreateCustomAsset ? : null} +
+ ) : null} + {visibleAssets.map((asset) => { + const balance = assetBalance(asset, balances); + const isFavorite = favorites.includes(asset.id); + const isDisabled = disabled.has(asset.id); + const assessment = assessAssetRisk(asset); + return ( +
+ + +
+ ); + })} +
+
+
+
+ ); +} diff --git a/frontend/src/components/tx/TransactionCenter.tsx b/frontend/src/components/tx/TransactionCenter.tsx new file mode 100644 index 000000000..0078edbae --- /dev/null +++ b/frontend/src/components/tx/TransactionCenter.tsx @@ -0,0 +1,29 @@ +import { dexRegistry } from "../../config/registry"; +import { useTxHistory } from "../../tx/TxHistoryContext"; + +const activeStatuses = new Set(["preparing", "awaiting-signature", "submitted"]); + +export function TransactionCenter() { + const { records, dismiss, centerOpen, setCenterOpen } = useTxHistory(); + if (records.length === 0) return null; + const hasActiveTransaction = records.some((record) => activeStatuses.has(record.status)); + return ( + + ); +} diff --git a/frontend/src/components/tx/TxStatusDialog.test.tsx b/frontend/src/components/tx/TxStatusDialog.test.tsx new file mode 100644 index 000000000..443c23d40 --- /dev/null +++ b/frontend/src/components/tx/TxStatusDialog.test.tsx @@ -0,0 +1,40 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { TxStatusDialog } from "./TxStatusDialog"; +import type { TxLifecycleState } from "../../tx/useTxRunner"; + +const result = { + transactionHash: "ABC123DEF456", +} as TxLifecycleState["result"]; + +describe("TxStatusDialog", () => { + it("renders pending/signing state", () => { + render(); + expect(screen.getByText("Transaction status")).toBeTruthy(); + expect(screen.getByText("Awaiting wallet signature")).toBeTruthy(); + expect(screen.getByText("Confirm in wallet")).toBeTruthy(); + }); + + it("renders success with tx hash", () => { + render(); + expect(screen.getByText("Transaction confirmed")).toBeTruthy(); + expect(screen.getByText("ABC123DEF456")).toBeTruthy(); + expect(screen.getByRole("link", { name: /view transaction in explorer/i }).getAttribute("href")).toContain("/tx/ABC123DEF456"); + }); + + it("renders failure copy with retry affordance", () => { + const retry = vi.fn(); + render(); + expect(screen.getByText("Transaction failed")).toBeTruthy(); + expect(screen.getByText("Insufficient funds")).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: /retry transaction/i })); + expect(retry).toHaveBeenCalledTimes(1); + }); + + it("renders rejected state without a tx hash", () => { + render(); + expect(screen.getByText("Rejected in wallet")).toBeTruthy(); + expect(screen.getByText("Transaction rejected")).toBeTruthy(); + expect(screen.queryByText(/Tx hash:/i)).toBeNull(); + }); +}); diff --git a/frontend/src/components/tx/TxStatusDialog.tsx b/frontend/src/components/tx/TxStatusDialog.tsx new file mode 100644 index 000000000..c3d6b20be --- /dev/null +++ b/frontend/src/components/tx/TxStatusDialog.tsx @@ -0,0 +1,57 @@ +import { decodeTxError, type DecodedTxError } from "../../tx/errors"; +import { TxHashLink, txLifecycleLabel, type TxLifecycleState, type TxLifecycleStatus, type TxResult } from "../../tx/useTxRunner"; + +type LegacyTxStatusDialogProps = { + status: TxLifecycleStatus | string; + result?: TxResult; + error?: unknown; + retry?: () => void | Promise; +}; + +type TxStatusDialogProps = LegacyTxStatusDialogProps | { + state: TxLifecycleState; +}; + +function normalizeProps(props: TxStatusDialogProps): TxLifecycleState { + if ("state" in props) return props.state; + const status = props.status as TxLifecycleStatus; + const decodedError: DecodedTxError | undefined = props.error ? decodeTxError(props.error) : undefined; + return { + status, + label: txLifecycleLabel(status) ?? props.status, + result: props.result, + error: decodedError, + description: decodedError?.message, + retry: props.retry, + }; +} + +export function TxStatusDialog(props: TxStatusDialogProps) { + const state = normalizeProps(props); + if (state.status === "idle") return null; + + const txHash = state.result?.transactionHash; + const canRetry = Boolean(state.retry && state.status !== "confirmed"); + + return ( +
+ Transaction status +

{state.label}

+ {state.description ?

{state.description}

: null} + {txHash ? ( +

+ Tx hash: +

+ ) : null} + {state.error ? ( +
+ {state.error.title} +

{state.error.message}

+ {state.error.raw} +
+ ) : null} + {canRetry ? : null} + {state.refresh ? : null} +
+ ); +} diff --git a/frontend/src/components/wallet/ChainStatusBadge.test.tsx b/frontend/src/components/wallet/ChainStatusBadge.test.tsx new file mode 100644 index 000000000..e5feed72b --- /dev/null +++ b/frontend/src/components/wallet/ChainStatusBadge.test.tsx @@ -0,0 +1,47 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ChainStatusBadge } from "./ChainStatusBadge"; + +function renderBadge(rpcEndpoint = "https://primary.invalid") { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + , + ); +} + +describe("ChainStatusBadge", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("warns when the primary RPC is degraded and a fallback responds", async () => { + const fetchMock = vi.mocked(fetch); + fetchMock + .mockResolvedValueOnce({ ok: false, status: 503 } as Response) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ result: { sync_info: { latest_block_height: "12345" } } }), + } as Response); + + renderBadge("https://primary-fallback.invalid"); + + await waitFor(() => expect(screen.getByText(/Fallback RPC · Block 12345/i)).toBeTruthy()); + expect(screen.getByText(/Fallback RPC/i).className).toContain("status-warn"); + }); + + it("shows degraded when all configured RPC endpoints fail", async () => { + const fetchMock = vi.mocked(fetch); + fetchMock.mockResolvedValue({ ok: false, status: 503 } as Response); + + renderBadge("https://primary-down.invalid"); + + await waitFor(() => expect(screen.getByText(/RPC degraded/i)).toBeTruthy(), { timeout: 2_000 }); + }); +}); diff --git a/frontend/src/components/wallet/ChainStatusBadge.tsx b/frontend/src/components/wallet/ChainStatusBadge.tsx new file mode 100644 index 000000000..bb28c29fa --- /dev/null +++ b/frontend/src/components/wallet/ChainStatusBadge.tsx @@ -0,0 +1,46 @@ +import { useQuery } from "@tanstack/react-query"; +import { JUNO_CHAIN_INFO } from "../../config/chains"; + +export function ChainStatusBadge({ rpcEndpoint }: { rpcEndpoint: string }) { + const fallbackEndpoints = JUNO_CHAIN_INFO.fallbackRpcs; + const status = useQuery({ + queryKey: ["rpc-status", rpcEndpoint, fallbackEndpoints], + queryFn: async () => { + const check = async (endpoint: string) => { + const response = await fetch(`${endpoint}/status`); + if (!response.ok) throw new Error(`RPC status ${response.status}`); + const json = await response.json() as { result?: { sync_info?: { latest_block_height?: string } } }; + return json.result?.sync_info?.latest_block_height ?? "unknown"; + }; + + try { + return { height: await check(rpcEndpoint), fallback: false }; + } catch (primaryError) { + for (const endpoint of fallbackEndpoints) { + try { + return { height: await check(endpoint), fallback: true, endpoint }; + } catch { + // Try the next configured endpoint before surfacing degraded status. + } + } + throw primaryError; + } + }, + retry: 1, + staleTime: 30_000, + }); + + const isFallback = Boolean(status.data?.fallback); + + // Chrome only earns its space when something is wrong. A healthy RPC says nothing. + if (status.isLoading || (!status.isError && !isFallback)) return null; + + return ( + + {status.isError ? "RPC degraded" : `Fallback RPC · Block ${status.data?.height}`} + + ); +} diff --git a/frontend/src/components/wallet/IndexerStatusBadge.test.tsx b/frontend/src/components/wallet/IndexerStatusBadge.test.tsx new file mode 100644 index 000000000..c481fc7d9 --- /dev/null +++ b/frontend/src/components/wallet/IndexerStatusBadge.test.tsx @@ -0,0 +1,45 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { IndexerStatusBadge } from "./IndexerStatusBadge"; + +function renderBadge() { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render(); +} + +describe("IndexerStatusBadge", () => { + beforeEach(() => vi.stubGlobal("fetch", vi.fn())); + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + it("labels an unconfigured indexer without implying health", () => { + vi.stubEnv("VITE_DEX_INDEXER_URL", ""); + renderBadge(); + expect(screen.getByText("Indexer not configured")).toBeTruthy(); + }); + + it("renders no chrome at all when the indexer is healthy", async () => { + vi.stubEnv("VITE_DEX_INDEXER_URL", "https://indexer.invalid"); + vi.mocked(fetch).mockResolvedValue({ ok: true, json: async () => ({ status: "ok", service: "indexer", dataSource: "indexer", isMock: false }) } as Response); + const { container } = renderBadge(); + await waitFor(() => expect(vi.mocked(fetch)).toHaveBeenCalled()); + expect(container.textContent).toBe(""); + }); + + it("surfaces an unreachable indexer", async () => { + vi.stubEnv("VITE_DEX_INDEXER_URL", "https://indexer.invalid"); + vi.mocked(fetch).mockRejectedValue(new Error("network down")); + renderBadge(); + await waitFor(() => expect(screen.getByText("Indexer unavailable")).toBeTruthy(), { timeout: 5_000 }); + }); + + it("labels mock-backed health as preview data", async () => { + vi.stubEnv("VITE_DEX_INDEXER_URL", "https://indexer.invalid"); + vi.mocked(fetch).mockResolvedValue({ ok: true, json: async () => ({ status: "ok", service: "indexer", dataSource: "mock", isMock: true }) } as Response); + renderBadge(); + await waitFor(() => expect(screen.getByText("Preview data")).toBeTruthy()); + }); +}); diff --git a/frontend/src/components/wallet/IndexerStatusBadge.tsx b/frontend/src/components/wallet/IndexerStatusBadge.tsx new file mode 100644 index 000000000..819204d27 --- /dev/null +++ b/frontend/src/components/wallet/IndexerStatusBadge.tsx @@ -0,0 +1,23 @@ +import { useQuery } from "@tanstack/react-query"; +import { getIndexerRuntimeConfig } from "../../lib/data-access/indexerFallback"; +import { createIndexerClient } from "../../lib/indexer/client"; + +export function IndexerStatusBadge() { + const config = getIndexerRuntimeConfig(); + const status = useQuery({ + queryKey: ["indexer-health", config.baseUrl, config.disabled], + enabled: Boolean(config.baseUrl) && !config.disabled, + queryFn: () => createIndexerClient({ baseUrl: config.baseUrl!, timeoutMs: config.timeoutMs }).health(), + retry: 1, + staleTime: 30_000, + refetchInterval: 60_000, + }); + + // Failure-only: a healthy indexer is the expected case and renders no chrome. + if (config.disabled) return Indexer disabled; + if (!config.baseUrl) return Indexer not configured; + if (status.isLoading) return null; + if (status.isError) return Indexer unavailable; + if (status.data?.isMock) return Preview data; + return null; +} diff --git a/frontend/src/components/wallet/NetworkGuardBanner.tsx b/frontend/src/components/wallet/NetworkGuardBanner.tsx new file mode 100644 index 000000000..2dd515175 --- /dev/null +++ b/frontend/src/components/wallet/NetworkGuardBanner.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { Button, Stack, Text } from "@interchain-ui/react"; +import { useNetworkGuard } from "../../wallet/WalletContext"; + +export function NetworkGuardBanner() { + const { network, switchToJuno } = useNetworkGuard(); + const [error, setError] = useState(); + + if (!network.message) return null; + + const recover = async () => { + setError(undefined); + try { + await switchToJuno(); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : "Unable to enable Juno in the selected wallet."); + } + }; + + return ( +
+ + {network.message} + + {error ? {error} : null} + +
+ ); +} diff --git a/frontend/src/components/wallet/WalletAddressActions.test.tsx b/frontend/src/components/wallet/WalletAddressActions.test.tsx new file mode 100644 index 000000000..3f58abdd1 --- /dev/null +++ b/frontend/src/components/wallet/WalletAddressActions.test.tsx @@ -0,0 +1,31 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WalletAddressActions } from "./WalletAddressActions"; + +const address = "juno1testwallet000000000000000000000000000000"; + +describe("WalletAddressActions", () => { + it("copies with navigator clipboard when available", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); + + render(); + fireEvent.click(screen.getByRole("button", { name: /copy wallet address/i })); + + await waitFor(() => expect(writeText).toHaveBeenCalledWith(address)); + expect(screen.getByText(/copied/i)).toBeTruthy(); + expect(screen.queryByRole("link")).toBeNull(); + }); + + it("falls back to document copy when clipboard is unavailable", async () => { + Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined }); + Object.defineProperty(document, "execCommand", { configurable: true, value: vi.fn(() => true) }); + const execCommand = vi.spyOn(document, "execCommand").mockReturnValue(true); + + render(); + fireEvent.click(screen.getByRole("button", { name: /copy wallet address/i })); + + await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy")); + execCommand.mockRestore(); + }); +}); diff --git a/frontend/src/components/wallet/WalletAddressActions.tsx b/frontend/src/components/wallet/WalletAddressActions.tsx new file mode 100644 index 000000000..5e6933a09 --- /dev/null +++ b/frontend/src/components/wallet/WalletAddressActions.tsx @@ -0,0 +1,46 @@ +import { useState } from "react"; +import { truncateAddress } from "../../lib/format/addresses"; + +async function copyText(value: string): Promise<"clipboard" | "fallback"> { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return "clipboard"; + } + + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.setAttribute("readonly", "true"); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + return "fallback"; +} + +export function WalletAddressActions({ address }: { address: string }) { + const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "failed">("idle"); + + return ( + event.stopPropagation()}> + {truncateAddress(address)} + + + ); +} diff --git a/frontend/src/components/wallet/WalletConnectButton.tsx b/frontend/src/components/wallet/WalletConnectButton.tsx new file mode 100644 index 000000000..131d58b82 --- /dev/null +++ b/frontend/src/components/wallet/WalletConnectButton.tsx @@ -0,0 +1,46 @@ +import { Button, Stack, Text } from "@interchain-ui/react"; +import { useEffect, useRef, useState } from "react"; +import { dexRegistry } from "../../config/registry"; +import { truncateAddress } from "../../lib/format/addresses"; +import { useWallet } from "../../wallet/WalletContext"; +import { WalletAddressActions } from "./WalletAddressActions"; + +export function WalletConnectButton() { + const { wallet, network, connect, disconnect, switchToJuno } = useWallet(); + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + useEffect(() => { + if (!menuOpen) return; + const close = (event: PointerEvent) => { + if (event.target instanceof Node && !menuRef.current?.contains(event.target)) setMenuOpen(false); + }; + const escape = (event: KeyboardEvent) => { if (event.key === "Escape") setMenuOpen(false); }; + document.addEventListener("pointerdown", close); + document.addEventListener("keydown", escape); + return () => { document.removeEventListener("pointerdown", close); document.removeEventListener("keydown", escape); }; + }, [menuOpen]); + + if (wallet.status === "connected" && wallet.address) { + return
+ + {menuOpen ?
+ + View account in explorer + {network.connectedChainId !== network.expectedChainId ? : null} + +
: null} +
; + } + + return ( + + + {wallet.status === "error" ? {wallet.error} : null} + + ); +} diff --git a/frontend/src/components/wallet/WalletTransactionHistory.test.tsx b/frontend/src/components/wallet/WalletTransactionHistory.test.tsx new file mode 100644 index 000000000..c27b316f9 --- /dev/null +++ b/frontend/src/components/wallet/WalletTransactionHistory.test.tsx @@ -0,0 +1,99 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { DataAccessState } from "../../lib/data-access/indexerFallback"; +import type { IndexerWalletTransaction } from "../../lib/indexer/types"; +import { formatAssetFlow, formatTimestamp, formatUsd, WalletTransactionHistory } from "./WalletTransactionHistory"; + +const indexedAccess: DataAccessState = { source: "indexer", isFallback: false, isMock: false, isStale: false }; +const txs: IndexerWalletTransaction[] = [ + { + txHash: "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890", + walletAddress: "juno1wallet", + poolId: "juno-usdc", + pairAddress: "juno1pool", + type: "swap", + height: 1234567, + timestamp: "2026-07-02T12:34:00.000Z", + offerAsset: { denom: "ujuno", symbol: "JUNO", amount: "12.5", valueUsd: 25 }, + askAsset: { denom: "ibc/usdc", symbol: "USDC", amount: "24.9", valueUsd: 24.9 }, + amountUsd: 24.9, + feeUsd: 0.07, + success: true, + dataSource: "indexer", + isMock: false, + }, + { + txHash: "WITHDRAW1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234", + walletAddress: "juno1wallet", + poolId: "juno-usdc", + pairAddress: "juno1pool", + type: "withdraw_liquidity", + height: 1234568, + timestamp: "2026-07-02T13:34:00.000Z", + offerAsset: { denom: "ujuno", symbol: "JUNO", amount: "1" }, + askAsset: { denom: "ibc/usdc", symbol: "USDC", amount: "2" }, + amountUsd: null, + feeUsd: null, + success: true, + dataSource: "indexer", + isMock: false, + }, +]; + +function renderHistory(props: Partial[0]> = {}) { + return render( + , + ); +} + +describe("WalletTransactionHistory", () => { + it("renders wallet history rows with values, fees, assets, and tx hashes", () => { + renderHistory(); + + expect(screen.getByRole("heading", { name: "Wallet transaction history" })).toBeTruthy(); + expect(screen.getByText("Swap")).toBeTruthy(); + expect(screen.getByText("Remove liquidity")).toBeTruthy(); + expect(screen.getByText("12.5 JUNO → 24.9 USDC")).toBeTruthy(); + expect(screen.getByText("$24.90")).toBeTruthy(); + expect(screen.getByText("Fee $0.07")).toBeTruthy(); + expect(screen.getByText("ABCDEF12…567890")).toBeTruthy(); + expect(screen.queryByText("indexer")).toBeNull(); + }); + + it("shows an honest empty state when the indexer returns no wallet history", () => { + renderHistory({ history: [] }); + + expect(screen.getByText("No indexed wallet transactions")).toBeTruthy(); + expect(screen.getByText(/No swap, add, withdraw, or claim activity was returned/i)).toBeTruthy(); + expect(screen.getByText(/No fake rows are shown/i)).toBeTruthy(); + }); + + it("shows unavailable copy when indexer history falls back after failure", () => { + renderHistory({ history: [], access: { source: "fallback", isFallback: true, isMock: false, isStale: false, error: { code: "network", message: "Indexer request failed" } } }); + + expect(screen.getByText("Wallet history unavailable")).toBeTruthy(); + expect(screen.getByText(/Wallet history is unavailable \(Indexer request failed\)/i)).toBeTruthy(); + }); + + it("filters by transaction type", () => { + renderHistory(); + + fireEvent.click(screen.getByRole("button", { name: "Withdraws" })); + + const table = screen.getByRole("table", { name: "Wallet transaction history" }); + expect(within(table).queryByText("Swap")).toBeNull(); + expect(within(table).getByText("Remove liquidity")).toBeTruthy(); + }); + + it("formats transaction values and timestamps without inventing missing data", () => { + expect(formatUsd(1234.56)).toBe("$1,235"); + expect(formatUsd(null)).toBeUndefined(); + expect(formatAssetFlow(txs[0])).toBe("12.5 JUNO → 24.9 USDC"); + expect(formatTimestamp("not-a-date")).toBe("Time unavailable"); + }); +}); diff --git a/frontend/src/components/wallet/WalletTransactionHistory.tsx b/frontend/src/components/wallet/WalletTransactionHistory.tsx new file mode 100644 index 000000000..2f0bd042c --- /dev/null +++ b/frontend/src/components/wallet/WalletTransactionHistory.tsx @@ -0,0 +1,174 @@ +import { useMemo, useState } from "react"; +import type { DataAccessState } from "../../lib/data-access/indexerFallback"; +import type { IndexerAssetAmount, IndexerWalletTransaction } from "../../lib/indexer/types"; +import type { RegistryPool } from "../../config/registry"; +import { formatAmount } from "../../lib/format/amounts"; +import { EmptyState, ExplorerLink, Skeleton } from "../common"; + +export type WalletTransactionTypeFilter = "all" | "swap" | "provide_liquidity" | "withdraw_liquidity" | "claim_rewards"; + +const TYPE_OPTIONS: { value: WalletTransactionTypeFilter; label: string }[] = [ + { value: "all", label: "All" }, + { value: "swap", label: "Swaps" }, + { value: "provide_liquidity", label: "Adds" }, + { value: "withdraw_liquidity", label: "Withdraws" }, + { value: "claim_rewards", label: "Claims" }, +]; + +const TYPE_LABELS: Record = { + swap: "Swap", + provide_liquidity: "Add liquidity", + withdraw_liquidity: "Remove liquidity", + claim_rewards: "Claim rewards", +}; + +type WalletTransactionHistoryProps = { + history: readonly IndexerWalletTransaction[]; + access?: DataAccessState; + explorerBaseUrl?: string; + walletConnected: boolean; + isLoading?: boolean; + pairAddress?: string; + title?: string; + emptyTitle?: string; + pool?: RegistryPool; +}; + +export function WalletTransactionHistory({ + history, + access, + walletConnected, + isLoading = false, + pairAddress, + explorerBaseUrl, + pool, + title = "Wallet transaction history", + emptyTitle, +}: WalletTransactionHistoryProps) { + const [typeFilter, setTypeFilter] = useState("all"); + const filtered = useMemo(() => history + .filter((tx) => !pairAddress || tx.pairAddress === pairAddress || tx.poolId === pairAddress) + .filter((tx) => typeFilter === "all" || tx.type === typeFilter), [history, pairAddress, typeFilter]); + const hasIndexerFailure = Boolean(access?.isFallback || access?.source === "disabled" || access?.error); + + return ( +
+
+
+

Activity

+

{title}

+

Recent swaps, add/remove liquidity, and reward claims appear here when wallet activity is available.

+
+
+ +
+ {TYPE_OPTIONS.map((option) => ( + + ))} +
+ + {!walletConnected ? ( + Wallet activity is shown per connected address. + ) : isLoading ? ( +
+ + + +
+ ) : hasIndexerFailure ? ( + {access?.error ? `Wallet history is unavailable (${access.error.message}).` : "Wallet history is unavailable."} + ) : filtered.length === 0 ? ( + No {typeFilter === "all" ? "swap, add, withdraw, or claim" : TYPE_OPTIONS.find((option) => option.value === typeFilter)?.label.toLowerCase()} activity was returned for this {pairAddress ? "pool" : "wallet"}. No fake rows are shown. + ) : ( +
+
+ Time + Type + Pool / assets + Value / fee + Tx +
+ {filtered.map((tx) => ( +
+
{formatTimestamp(tx.timestamp)}Height {tx.height.toLocaleString()}
+
{formatType(tx.type)}{!tx.success ? failed : null}
+
{formatAssetFlow(tx, pool)}{pool?.assets.map((asset) => asset.symbol).join(" / ") ?? "Pool"}
+
{formatUsd(tx.amountUsd) ?? "—"}{formatUsd(tx.feeUsd) ? Fee {formatUsd(tx.feeUsd)} : null}
+
+ {explorerBaseUrl ? {shortHash(tx.txHash)} : {shortHash(tx.txHash)}} + +
+
+ ))} +
+ )} +
+ ); +} + +function formatType(type: string) { + return TYPE_LABELS[type] ?? type.replace(/_/g, " "); +} + +function txAssets(tx: IndexerWalletTransaction): IndexerAssetAmount[] { + const dynamic = tx as IndexerWalletTransaction & { assets?: IndexerAssetAmount[]; rewards?: IndexerAssetAmount[]; withdrawnAssets?: IndexerAssetAmount[]; providedAssets?: IndexerAssetAmount[] }; + const listed = dynamic.assets ?? dynamic.rewards ?? dynamic.withdrawnAssets ?? dynamic.providedAssets; + if (listed?.length) return listed; + return [tx.offerAsset, tx.askAsset].filter((asset): asset is IndexerAssetAmount => Boolean(asset)); +} + +export function formatAssetFlow(tx: IndexerWalletTransaction, pool?: RegistryPool) { + if (tx.type === "swap" && tx.offerAsset && tx.askAsset) { + return `${formatAssetAmount(tx.offerAsset, pool)} → ${formatAssetAmount(tx.askAsset, pool)}`; + } + const assets = txAssets(tx).map((asset) => formatAssetAmount(asset, pool)).filter(Boolean); + if (assets.length > 0) return assets.join(" + "); + return "Assets unavailable"; +} + +export function formatAssetAmount(asset: IndexerAssetAmount, pool?: RegistryPool) { + const amount = asset.amount ?? asset.reserve; + const registryAsset = pool?.assets.find((candidate) => candidate.id === asset.denom || candidate.denomTrace === asset.denom); + const symbol = registryAsset?.symbol ?? asset.symbol ?? denomTicker(asset.denom); + if (!amount) return symbol; + const displayAmount = registryAsset && /^\d+$/.test(amount) + ? formatAmount(amount, registryAsset.decimals) + : formatCompactDecimal(amount); + return `${displayAmount} ${symbol}`; +} + +function denomTicker(denom: string) { + if (denom === "ujuno") return "JUNO"; + if (denom.startsWith("factory/")) return denom.split("/").at(-1)?.replace(/^u/, "").toUpperCase() ?? "TOKEN"; + if (/^u[a-z0-9]+$/i.test(denom)) return denom.slice(1).toUpperCase(); + return denom.length > 16 ? "TOKEN" : denom.toUpperCase(); +} + +export function formatTimestamp(timestamp: string) { + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) return "Time unavailable"; + return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(date); +} + +export function formatUsd(value: number | null | undefined) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: value >= 100 ? 0 : 2 }).format(value); +} + +function formatCompactDecimal(value: string) { + const numeric = Number(value); + if (!Number.isFinite(numeric)) return value; + return new Intl.NumberFormat("en-US", { maximumFractionDigits: 6 }).format(numeric); +} + +function shortHash(hash: string) { + return hash.length > 14 ? `${hash.slice(0, 8)}…${hash.slice(-6)}` : hash; +} diff --git a/frontend/src/config/chains.ts b/frontend/src/config/chains.ts new file mode 100644 index 000000000..64541fffa --- /dev/null +++ b/frontend/src/config/chains.ts @@ -0,0 +1,40 @@ +export const JUNO_CHAIN_INFO = { + chainId: "juno-1", + chainName: "Juno", + rpc: "https://juno-rpc.publicnode.com:443", + fallbackRpcs: ["https://rpc-juno-ia.cosmosia.notional.ventures"], + rest: "https://juno-rest.publicnode.com", + bip44: { coinType: 118 }, + bech32Config: { + bech32PrefixAccAddr: "juno", + bech32PrefixAccPub: "junopub", + bech32PrefixValAddr: "junovaloper", + bech32PrefixValPub: "junovaloperpub", + bech32PrefixConsAddr: "junovalcons", + bech32PrefixConsPub: "junovalconspub", + }, + currencies: [ + { + coinDenom: "JUNO", + coinMinimalDenom: "ujuno", + coinDecimals: 6, + coinGeckoId: "juno-network", + }, + ], + feeCurrencies: [ + { + coinDenom: "JUNO", + coinMinimalDenom: "ujuno", + coinDecimals: 6, + coinGeckoId: "juno-network", + gasPriceStep: { low: 0.075, average: 0.075, high: 0.1 }, + }, + ], + stakeCurrency: { + coinDenom: "JUNO", + coinMinimalDenom: "ujuno", + coinDecimals: 6, + coinGeckoId: "juno-network", + }, + features: ["cosmwasm", "ibc-transfer"], +} as const; diff --git a/frontend/src/config/cosmosKit.ts b/frontend/src/config/cosmosKit.ts new file mode 100644 index 000000000..57b4d628e --- /dev/null +++ b/frontend/src/config/cosmosKit.ts @@ -0,0 +1,49 @@ +import { JUNO_CHAIN_INFO } from "./chains"; + +export const COSMOS_KIT_CHAIN_NAME = "juno"; + +export const junoChain = { + chain_name: COSMOS_KIT_CHAIN_NAME, + chain_type: "cosmos", + chain_id: JUNO_CHAIN_INFO.chainId, + pretty_name: JUNO_CHAIN_INFO.chainName, + status: "live", + network_type: "mainnet", + bech32_prefix: JUNO_CHAIN_INFO.bech32Config.bech32PrefixAccAddr, + bech32_config: JUNO_CHAIN_INFO.bech32Config, + slip44: JUNO_CHAIN_INFO.bip44.coinType, + fees: { + fee_tokens: JUNO_CHAIN_INFO.feeCurrencies.map((currency) => ({ + denom: currency.coinMinimalDenom, + low_gas_price: currency.gasPriceStep.low, + average_gas_price: currency.gasPriceStep.average, + high_gas_price: currency.gasPriceStep.high, + })), + }, + staking: { + staking_tokens: [{ denom: JUNO_CHAIN_INFO.stakeCurrency.coinMinimalDenom }], + }, + codebase: { + cosmwasm_enabled: JUNO_CHAIN_INFO.features.includes("cosmwasm"), + }, + apis: { + rpc: [{ address: JUNO_CHAIN_INFO.rpc, provider: "itastakers" }], + rest: [{ address: JUNO_CHAIN_INFO.rest, provider: "itastakers" }], + }, +} as const; + +export const junoAssetList = { + chain_name: COSMOS_KIT_CHAIN_NAME, + assets: JUNO_CHAIN_INFO.currencies.map((currency) => ({ + base: currency.coinMinimalDenom, + name: currency.coinDenom, + display: currency.coinDenom.toLowerCase(), + symbol: currency.coinDenom, + coingecko_id: currency.coinGeckoId, + type_asset: "sdk.coin", + denom_units: [ + { denom: currency.coinMinimalDenom, exponent: 0 }, + { denom: currency.coinDenom.toLowerCase(), exponent: currency.coinDecimals }, + ], + })), +} as const; diff --git a/frontend/src/config/deployment.ts b/frontend/src/config/deployment.ts new file mode 100644 index 000000000..1855f48e4 --- /dev/null +++ b/frontend/src/config/deployment.ts @@ -0,0 +1,15 @@ +import { dexRegistry } from "./registry"; + +export const junoDeployment = { + chainId: dexRegistry.chainId, + rpcEndpoint: dexRegistry.rpcEndpoint, + restEndpoint: dexRegistry.restEndpoint, + explorerBaseUrl: dexRegistry.explorerBaseUrl, + contracts: { + factory: dexRegistry.factory, + nativeCoinRegistry: dexRegistry.nativeCoinRegistry, + router: dexRegistry.router, + incentives: dexRegistry.incentives, + oracle: dexRegistry.oracle, + }, +} as const; diff --git a/frontend/src/config/registry.test.ts b/frontend/src/config/registry.test.ts new file mode 100644 index 000000000..715c44dcf --- /dev/null +++ b/frontend/src/config/registry.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import { applyDexRegistryEnvOverrides, dexRegistry, enabledPools, isPoolTradeable, parseDexRegistry } from "./registry"; + +describe("dex registry", () => { + it("loads the committed juno-1 registry", () => { + expect(dexRegistry.chainId).toBe("juno-1"); + expect(dexRegistry.pools).toHaveLength(5); + expect(dexRegistry.pools[0].type).toBe("xyk"); + expect(dexRegistry.pools.every((pool) => pool.status === "active" && pool.verified === true)).toBe(true); + expect(enabledPools).toHaveLength(5); + expect(dexRegistry.pools.map((pool) => pool.id)).toEqual(expect.arrayContaining([ + "season0-twolf-juno", + "season0-traw-juno", + "season0-tahab-juno", + "season0-tahab-tfud", + ])); + }); + + it("requires explicit pool lifecycle and verification metadata", () => { + const pool = dexRegistry.pools[0]; + const { status: _status, ...withoutStatus } = pool; + const { verified: _poolVerified, ...withoutPoolVerification } = pool; + const { verified: _assetVerified, ...withoutAssetVerification } = pool.assets[0]; + + expect(() => parseDexRegistry({ ...dexRegistry, pools: [withoutStatus] })).toThrow(/status/i); + expect(() => parseDexRegistry({ ...dexRegistry, pools: [withoutPoolVerification] })).toThrow(/verified/i); + expect(() => parseDexRegistry({ + ...dexRegistry, + pools: [{ ...pool, assets: [withoutAssetVerification, pool.assets[1]] }], + })).toThrow(/verified/i); + }); + + it("allows normal trading only for enabled active pools", () => { + const pool = dexRegistry.pools[0]; + expect(isPoolTradeable(pool)).toBe(true); + expect(isPoolTradeable({ ...pool, status: "experimental" })).toBe(false); + expect(isPoolTradeable({ ...pool, status: "active", enabled: false })).toBe(false); + expect(isPoolTradeable({ ...pool, status: "active", assets: [{ ...pool.assets[0], blocked: true }, pool.assets[1]] })).toBe(false); + }); + + it("validates explicit blocked asset metadata", () => { + const pool = dexRegistry.pools[0]; + expect(parseDexRegistry({ + ...dexRegistry, + pools: [{ ...pool, assets: [{ ...pool.assets[0], blocked: true }, pool.assets[1]] }], + }).pools[0].assets[0].blocked).toBe(true); + expect(() => parseDexRegistry({ + ...dexRegistry, + pools: [{ ...pool, assets: [{ ...pool.assets[0], blocked: "yes" }, pool.assets[1]] }], + })).toThrow(/blocked/i); + }); + + it("rejects placeholder contract addresses", () => { + const invalid = { + ...dexRegistry, + factory: "juno1replacefactory000000000000000000000000000000", + }; + + expect(() => parseDexRegistry(invalid)).toThrow(/placeholder/i); + }); + + it("rejects non-juno chain registries", () => { + expect(() => parseDexRegistry({ ...dexRegistry, chainId: "uni-7" })).toThrow(/juno-1/); + }); + + it("accepts curated stable and concentrated pools without allowing unknown pool types", () => { + const stableRegistry = { + ...dexRegistry, + pools: [{ ...dexRegistry.pools[0], id: "stable-pool", type: "stable" }], + }; + const concentratedRegistry = { + ...dexRegistry, + pools: [{ ...dexRegistry.pools[0], id: "concentrated-pool", type: "concentrated" }], + }; + + expect(parseDexRegistry(stableRegistry).pools[0].type).toBe("stable"); + expect(parseDexRegistry(concentratedRegistry).pools[0].type).toBe("concentrated"); + expect(() => parseDexRegistry({ ...dexRegistry, pools: [{ ...dexRegistry.pools[0], type: "placeholder" }] })).toThrow(/xyk, stable, or concentrated/); + }); + + it("allows deploy environments to override public endpoints", () => { + import.meta.env.VITE_DEX_RPC_URL = "https://rpc.host.invalid"; + import.meta.env.VITE_DEX_REST_URL = "https://rest.host.invalid"; + import.meta.env.VITE_DEX_EXPLORER_URL = "https://explorer.host.invalid/juno"; + + const overridden = applyDexRegistryEnvOverrides(dexRegistry); + + expect(overridden.rpcEndpoint).toBe("https://rpc.host.invalid"); + expect(overridden.restEndpoint).toBe("https://rest.host.invalid"); + expect(overridden.explorerBaseUrl).toBe("https://explorer.host.invalid/juno"); + + delete import.meta.env.VITE_DEX_RPC_URL; + delete import.meta.env.VITE_DEX_REST_URL; + delete import.meta.env.VITE_DEX_EXPLORER_URL; + }); +}); diff --git a/frontend/src/config/registry.ts b/frontend/src/config/registry.ts new file mode 100644 index 000000000..d3892b5ae --- /dev/null +++ b/frontend/src/config/registry.ts @@ -0,0 +1,190 @@ +import registryJson from "../data/registry.juno-1.json"; +import { mergeAssetMetadata } from "../lib/assets/assetMetadata"; + +export type RegistryAsset = { + kind: "native" | "ibc" | "cw20"; + id: string; + symbol: string; + name?: string; + display?: string; + decimals: number; + denomTrace?: string; + logoURI?: string; + coingeckoId?: string; + trace?: { + path?: string; + channelId?: string; + counterpartyChainName?: string; + counterpartyBaseDenom?: string; + counterpartyChannelId?: string; + }; + verified?: boolean; + blocked?: boolean; +}; + +export type PoolLifecycle = "experimental" | "active" | "deprecated" | "blocked"; + +export type RegistryPool = { + id: string; + label: string; + pair: string; + lpToken: string; + type: "xyk" | "stable" | "concentrated"; + feeBps: number; + assets: [RegistryAsset, RegistryAsset]; + explorer: string; + enabled: boolean; + status: PoolLifecycle; + featured?: boolean; + notes?: string; + source?: "registry" | "factory"; + verified?: boolean; +}; + +export type DexRegistry = { + chainId: "juno-1"; + chainName: string; + rpcEndpoint: string; + restEndpoint: string; + explorerBaseUrl: string; + factory: string; + nativeCoinRegistry: string; + router?: string; + incentives?: string; + oracle?: string; + updatedAt: string; + pools: RegistryPool[]; +}; + +const PLACEHOLDER_PATTERN = /replace|placeholder|todo|example|changeme|xxx|000000000000/i; +const JUNO_ADDRESS_PATTERN = /^juno1[ac-hj-np-z02-9]{38,58}$/; + +function assertRecord(value: unknown, label: string): asserts value is Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } +} + +function assertString(value: unknown, label: string): asserts value is string { + if (typeof value !== "string" || value.trim() === "") { + throw new Error(`${label} must be a non-empty string`); + } + if (PLACEHOLDER_PATTERN.test(value)) { + throw new Error(`${label} contains a placeholder value`); + } +} + +function assertJunoAddress(value: unknown, label: string): asserts value is string { + assertString(value, label); + if (!JUNO_ADDRESS_PATTERN.test(value)) { + throw new Error(`${label} must be a juno bech32 address`); + } +} + +function parseAsset(value: unknown, label: string): RegistryAsset { + assertRecord(value, label); + if (value.kind !== "native" && value.kind !== "ibc" && value.kind !== "cw20") { + throw new Error(`${label}.kind must be native, ibc, or cw20`); + } + assertString(value.id, `${label}.id`); + assertString(value.symbol, `${label}.symbol`); + if (typeof value.decimals !== "number" || !Number.isInteger(value.decimals) || value.decimals < 0) { + throw new Error(`${label}.decimals must be a non-negative integer`); + } + if (value.kind === "cw20") { + assertJunoAddress(value.id, `${label}.id`); + } + if (typeof value.name !== "undefined") assertString(value.name, `${label}.name`); + if (typeof value.display !== "undefined") assertString(value.display, `${label}.display`); + if (typeof value.logoURI !== "undefined") assertString(value.logoURI, `${label}.logoURI`); + if (typeof value.denomTrace !== "undefined") assertString(value.denomTrace, `${label}.denomTrace`); + if (typeof value.coingeckoId !== "undefined") assertString(value.coingeckoId, `${label}.coingeckoId`); + if (typeof value.trace !== "undefined") assertRecord(value.trace, `${label}.trace`); + if (typeof value.verified !== "boolean") throw new Error(`${label}.verified must be an explicit boolean`); + if (typeof value.blocked !== "undefined" && typeof value.blocked !== "boolean") throw new Error(`${label}.blocked must be boolean when provided`); + return value as RegistryAsset; +} + +function parsePool(value: unknown, index: number): RegistryPool { + const label = `pools[${index}]`; + assertRecord(value, label); + assertString(value.id, `${label}.id`); + assertString(value.label, `${label}.label`); + assertJunoAddress(value.pair, `${label}.pair`); + assertString(value.lpToken, `${label}.lpToken`); + if (value.type !== "xyk" && value.type !== "stable" && value.type !== "concentrated") { + throw new Error(`${label}.type must be xyk, stable, or concentrated`); + } + if (typeof value.feeBps !== "number" || value.feeBps < 0 || value.feeBps > 10_000) { + throw new Error(`${label}.feeBps must be between 0 and 10000`); + } + if (!Array.isArray(value.assets) || value.assets.length !== 2) { + throw new Error(`${label}.assets must contain exactly two assets`); + } + assertString(value.explorer, `${label}.explorer`); + if (!value.explorer.startsWith("https://")) throw new Error(`${label}.explorer must be https`); + if (typeof value.enabled !== "boolean") throw new Error(`${label}.enabled must be boolean`); + if (value.status !== "experimental" && value.status !== "active" && value.status !== "deprecated" && value.status !== "blocked") { + throw new Error(`${label}.status must be experimental, active, deprecated, or blocked`); + } + if (typeof value.verified !== "boolean") throw new Error(`${label}.verified must be an explicit boolean`); + return { + ...value, + assets: [parseAsset(value.assets[0], `${label}.assets[0]`), parseAsset(value.assets[1], `${label}.assets[1]`)], + } as RegistryPool; +} + +export function parseDexRegistry(value: unknown): DexRegistry { + assertRecord(value, "registry"); + if (value.chainId !== "juno-1") throw new Error("registry.chainId must be juno-1"); + assertString(value.chainName, "registry.chainName"); + assertString(value.rpcEndpoint, "registry.rpcEndpoint"); + assertString(value.restEndpoint, "registry.restEndpoint"); + assertString(value.explorerBaseUrl, "registry.explorerBaseUrl"); + assertJunoAddress(value.factory, "registry.factory"); + assertJunoAddress(value.nativeCoinRegistry, "registry.nativeCoinRegistry"); + if (value.router) assertJunoAddress(value.router, "registry.router"); + if (value.incentives) assertJunoAddress(value.incentives, "registry.incentives"); + if (value.oracle) assertJunoAddress(value.oracle, "registry.oracle"); + assertString(value.updatedAt, "registry.updatedAt"); + if (!Array.isArray(value.pools)) throw new Error("registry.pools must be an array"); + const pools = value.pools.map(parsePool); + const ids = new Set(); + for (const pool of pools) { + if (ids.has(pool.id)) throw new Error(`duplicate pool id: ${pool.id}`); + ids.add(pool.id); + } + return { ...value, pools } as DexRegistry; +} + +function withChainRegistryMetadata(registry: DexRegistry): DexRegistry { + return { + ...registry, + pools: registry.pools.map((pool) => ({ + ...pool, + assets: [mergeAssetMetadata(pool.assets[0]), mergeAssetMetadata(pool.assets[1])], + })), + }; +} + +function envString(name: string): string | undefined { + const value = import.meta.env[name] as string | undefined; + return value?.trim() || undefined; +} + +export function applyDexRegistryEnvOverrides(registry: DexRegistry): DexRegistry { + return parseDexRegistry({ + ...registry, + rpcEndpoint: envString("VITE_DEX_RPC_URL") ?? registry.rpcEndpoint, + restEndpoint: envString("VITE_DEX_REST_URL") ?? registry.restEndpoint, + explorerBaseUrl: envString("VITE_DEX_EXPLORER_URL") ?? registry.explorerBaseUrl, + }); +} + +export const dexRegistry = withChainRegistryMetadata(applyDexRegistryEnvOverrides(parseDexRegistry(registryJson))); +export function isPoolTradeable(pool: RegistryPool): boolean { + return pool.enabled && pool.status === "active" && pool.assets.every((asset) => asset.blocked !== true); +} + +export const configuredPools = dexRegistry.pools.filter((pool) => pool.enabled); +export const enabledPools = configuredPools.filter(isPoolTradeable); diff --git a/frontend/src/data/chain-registry-assets.juno-1.json b/frontend/src/data/chain-registry-assets.juno-1.json new file mode 100644 index 000000000..4fb53d086 --- /dev/null +++ b/frontend/src/data/chain-registry-assets.juno-1.json @@ -0,0 +1,1035 @@ +{ + "$schema": "./chain-registry-assets.schema.json", + "chainId": "juno-1", + "source": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/assetlist.json", + "generatedAt": "2026-07-02T15:19:15.138Z", + "assets": [ + { + "denom": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/twolf", + "kind": "factory", + "symbol": "TWOLF", + "name": "Juno Meme Season 0 Test Wolf", + "display": "twolf", + "decimals": 6 + }, + { + "denom": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/traw", + "kind": "factory", + "symbol": "TRAW", + "name": "Juno Meme Season 0 Test RAW", + "display": "traw", + "decimals": 6 + }, + { + "denom": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/tahab", + "kind": "factory", + "symbol": "TAHAB", + "name": "Juno Meme Season 0 Test AHAB", + "display": "tahab", + "decimals": 6 + }, + { + "denom": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/tfud", + "kind": "factory", + "symbol": "TFUD", + "name": "Juno Meme Season 0 Test FUD", + "display": "tfud", + "decimals": 6 + }, + { + "denom": "factory/juno16uprl38e4ljj5ctuha9ehpvp2l93z3d5jmwj2cttt6jkhlrhscpqgglalk/wind.ash", + "kind": "factory", + "symbol": "ashWIND", + "name": "ashWIND", + "display": "ashWIND", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/wind.ash.png" + }, + { + "denom": "factory/juno1h6y8tkceau4d8zyv5aa0fwdj2pa2y0gz2hx0tq/uwind", + "kind": "factory", + "symbol": "WIND", + "name": "Wind Token", + "display": "wind", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/wind.png" + }, + { + "denom": "factory/juno1qly4zcmzr2gyxtze5yt9chv2srczwwunppxjfh/NEXX", + "kind": "factory", + "symbol": "NEXX", + "name": "NEXX GEN AI", + "display": "nexx", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/nexx.png" + }, + { + "denom": "factory/juno1u805lv20qc6jy7c3ttre7nct6uyl20pfky5r7e/DGL", + "kind": "factory", + "symbol": "DGL", + "name": "Licorice", + "display": "dgl", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/dgl.png" + }, + { + "denom": "factory/juno1vwmnqk0vyxc96qgffrure4nqxupjrql0zut8s02hadgp0n79r8xq5xdsxy/ARENA", + "kind": "factory", + "symbol": "ARENA", + "name": "Arena Token", + "display": "arena", + "decimals": 6 + }, + { + "denom": "factory/juno1zjqsel42pj5e6wvxxw7hjs9gn06yqz4m3ffyua3x2v44m4l8trjsr92q9s/empr", + "kind": "factory", + "symbol": "EMPR", + "name": "EMPR", + "display": "EMPR", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/empr.png" + }, + { + "denom": "ibc/3A6ADE78FB8169C034C29C4F2E1A61CE596EC8235366F22381D981A98F1F5A5C", + "kind": "ibc", + "symbol": "WHALE", + "name": "Migaloo", + "display": "whale", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/migaloo/images/white-whale.svg", + "denomTrace": "transfer/channel-210/uwhale", + "trace": { + "path": "transfer/channel-210/uwhale", + "channelId": "channel-210", + "counterpartyChainName": "migaloo", + "counterpartyBaseDenom": "uwhale", + "counterpartyChannelId": "channel-1" + } + }, + { + "denom": "ibc/4A482FA914A4B9B05801ED81C33713899F322B24F76A06F4B8FE872485EA22FF", + "kind": "ibc", + "symbol": "USDC", + "name": "USD Coin", + "display": "usdc", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/usdc.svg", + "coingeckoId": "usd-coin", + "denomTrace": "transfer/channel-224/uusdc", + "trace": { + "path": "transfer/channel-224/uusdc", + "channelId": "channel-224", + "counterpartyChainName": "noble", + "counterpartyBaseDenom": "uusdc", + "counterpartyChannelId": "channel-3" + } + }, + { + "denom": "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9", + "kind": "ibc", + "symbol": "ATOM", + "name": "ATOM on Juno", + "display": "atom", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.svg", + "coingeckoId": "cosmos", + "denomTrace": "transfer/channel-1/uatom", + "trace": { + "path": "transfer/channel-1/uatom", + "channelId": "channel-1", + "counterpartyChainName": "cosmoshub", + "counterpartyBaseDenom": "uatom", + "counterpartyChannelId": "channel-207" + } + }, + { + "denom": "ibc/F0C440C8040E2FCCAC621D32D3A00D9B347C989D52CE869A91CB34D07B0021D2", + "kind": "ibc", + "symbol": "RSTK", + "name": "Restake DAO Token", + "display": "rstk", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/migaloo/images/rstk.svg", + "denomTrace": "transfer/channel-210/factory/migaloo1d0uma9qzcts4fzt7ml39xp44aut5k6qyjfzz4asalnecppppr3rsl52vvv/rstk", + "trace": { + "path": "transfer/channel-210/factory/migaloo1d0uma9qzcts4fzt7ml39xp44aut5k6qyjfzz4asalnecppppr3rsl52vvv/rstk", + "channelId": "channel-210", + "counterpartyChainName": "migaloo", + "counterpartyBaseDenom": "factory/migaloo1d0uma9qzcts4fzt7ml39xp44aut5k6qyjfzz4asalnecppppr3rsl52vvv/rstk", + "counterpartyChannelId": "channel-1" + } + }, + { + "denom": "juno10gthz5ufgrpuk5cscve2f0hjp56wgp90psqxcrqlg4m9mcu9dh8q4864xy", + "aliases": [ + "cw20:juno10gthz5ufgrpuk5cscve2f0hjp56wgp90psqxcrqlg4m9mcu9dh8q4864xy" + ], + "kind": "cw20", + "symbol": "KLEO", + "name": "Kleomedes", + "display": "kleo", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/kleomedes.png" + }, + { + "denom": "juno10vgf2u03ufcf25tspgn05l7j3tfg0j63ljgpffy98t697m5r5hmqaw95ux", + "aliases": [ + "cw20:juno10vgf2u03ufcf25tspgn05l7j3tfg0j63ljgpffy98t697m5r5hmqaw95ux" + ], + "kind": "cw20", + "symbol": "SLCA", + "name": "Silica", + "display": "silica", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/silica.png" + }, + { + "denom": "juno12etxwkxvms0uy9ak8g3pyq6a53myukufdnx82pakzmjmpm77a0ksr9gs5v", + "aliases": [ + "cw20:juno12etxwkxvms0uy9ak8g3pyq6a53myukufdnx82pakzmjmpm77a0ksr9gs5v" + ], + "kind": "cw20", + "symbol": "EMPWR", + "name": "EMPWR", + "display": "empwr", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/empwr.png" + }, + { + "denom": "juno12mcwmd6wqhledkjsurlfqtc8j0pedvxlcxw3gs4kh2qf808ehehsen8nmw", + "aliases": [ + "cw20:juno12mcwmd6wqhledkjsurlfqtc8j0pedvxlcxw3gs4kh2qf808ehehsen8nmw" + ], + "kind": "cw20", + "symbol": "YFD", + "name": "Y-Foundry DAO", + "display": "yfd", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/yfd.svg" + }, + { + "denom": "juno12wxyvtqe76x2a5jj6ckp2hfq8v32m6rvyyxwwufl2tksqvkt7whqczv6pa", + "aliases": [ + "cw20:juno12wxyvtqe76x2a5jj6ckp2hfq8v32m6rvyyxwwufl2tksqvkt7whqczv6pa" + ], + "kind": "cw20", + "symbol": "CATOM", + "name": "Catom", + "display": "catom", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/catom.png" + }, + { + "denom": "juno13c57ssxvlzefsj4v5spdz4m9r6c6s2far5npvmc9en7nz02xqjyqne40gk", + "aliases": [ + "cw20:juno13c57ssxvlzefsj4v5spdz4m9r6c6s2far5npvmc9en7nz02xqjyqne40gk" + ], + "kind": "cw20", + "symbol": "LUNO", + "name": "LUNO", + "display": "luno", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/luno.png" + }, + { + "denom": "juno13ca2g36ng6etcfhr9qxx352uw2n5e92np54thfkm3w3nzlhsgvwsjaqlyq", + "aliases": [ + "cw20:juno13ca2g36ng6etcfhr9qxx352uw2n5e92np54thfkm3w3nzlhsgvwsjaqlyq" + ], + "kind": "cw20", + "symbol": "MANNA", + "name": "Manna", + "display": "manna", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/manna.png" + }, + { + "denom": "juno13epyeat7ef0k7q6kllmyvc8zpfd9xm7cqjrgtk0qkgrk7n5mjfmq8979jw", + "aliases": [ + "cw20:juno13epyeat7ef0k7q6kllmyvc8zpfd9xm7cqjrgtk0qkgrk7n5mjfmq8979jw" + ], + "kind": "cw20", + "symbol": "POIL", + "name": "POIL", + "display": "poil", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/poil.png" + }, + { + "denom": "juno14fz92ehqt37e096xr95kmy8nc0kz803uezxtg4fwx7agjjma86sqm8mg3h", + "aliases": [ + "cw20:juno14fz92ehqt37e096xr95kmy8nc0kz803uezxtg4fwx7agjjma86sqm8mg3h" + ], + "kind": "cw20", + "symbol": "BITS", + "name": "BITS", + "display": "bits", + "decimals": 8, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/bits.png" + }, + { + "denom": "juno14lycavan8gvpjn97aapzvwmsj8kyrvf644p05r0hu79namyj3ens87650k", + "aliases": [ + "cw20:juno14lycavan8gvpjn97aapzvwmsj8kyrvf644p05r0hu79namyj3ens87650k" + ], + "kind": "cw20", + "symbol": "SGNL", + "name": "Signal", + "display": "sgnl", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/sgnl.png" + }, + { + "denom": "juno14q8kk464fafql2fwmlsgvgcdl6h2csqpzv4hr025fmcvgjahpess32k0j7", + "aliases": [ + "cw20:juno14q8kk464fafql2fwmlsgvgcdl6h2csqpzv4hr025fmcvgjahpess32k0j7" + ], + "kind": "cw20", + "symbol": "BLUE", + "name": "Blue", + "display": "blue", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/blue.png" + }, + { + "denom": "juno1525fuspletvzykpgr2atxpymu9le4mghd7qq4a4u23uwqzc2f3fq7fmafd", + "aliases": [ + "cw20:juno1525fuspletvzykpgr2atxpymu9le4mghd7qq4a4u23uwqzc2f3fq7fmafd" + ], + "kind": "cw20", + "symbol": "MIDDLE", + "name": "Middle", + "display": "middle", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/middle.png" + }, + { + "denom": "juno159q8t5g02744lxq8lfmcn6f78qqulq9wn3y9w7lxjgkz4e0a6kvsfvapse", + "aliases": [ + "cw20:juno159q8t5g02744lxq8lfmcn6f78qqulq9wn3y9w7lxjgkz4e0a6kvsfvapse" + ], + "kind": "cw20", + "symbol": "SOLAR", + "name": "Solarbank DAO", + "display": "solar", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/solar.svg" + }, + { + "denom": "juno15au4k2jgwd0jnchy0fkg3lm00fpt7jt0j2duuzradn2q7sega2dszyn5pp", + "aliases": [ + "cw20:juno15au4k2jgwd0jnchy0fkg3lm00fpt7jt0j2duuzradn2q7sega2dszyn5pp" + ], + "kind": "cw20", + "symbol": "PLTN", + "name": "Palantin", + "display": "pltn", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/pltn.png" + }, + { + "denom": "juno15u3dt79t6sxxa3x3kpkhzsy56edaa5a66wvt3kxmukqjz2sx0hes5sn38g", + "aliases": [ + "cw20:juno15u3dt79t6sxxa3x3kpkhzsy56edaa5a66wvt3kxmukqjz2sx0hes5sn38g" + ], + "kind": "cw20", + "symbol": "RAW", + "name": "JunoSwap", + "display": "raw", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/raw.svg" + }, + { + "denom": "juno166heaxlyntd33a5euh4rrz26svhean4klzw594esmd02l4atan6sazy2my", + "aliases": [ + "cw20:juno166heaxlyntd33a5euh4rrz26svhean4klzw594esmd02l4atan6sazy2my" + ], + "kind": "cw20", + "symbol": "MNPU", + "name": "Mini Punks", + "display": "mnpu", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/mnpu.svg" + }, + { + "denom": "juno168ctmpyppk90d34p3jjy658zf5a5l3w8wk35wht6ccqj4mr0yv8s4j5awr", + "aliases": [ + "cw20:juno168ctmpyppk90d34p3jjy658zf5a5l3w8wk35wht6ccqj4mr0yv8s4j5awr" + ], + "kind": "cw20", + "symbol": "NETA", + "name": "Neta", + "display": "neta", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/neta.svg", + "coingeckoId": "neta" + }, + { + "denom": "juno17703kcxtsg37hryxnestejyyycuv5yyvnghp2e7w0kqvafnnyetsgzq62w", + "aliases": [ + "cw20:juno17703kcxtsg37hryxnestejyyycuv5yyvnghp2e7w0kqvafnnyetsgzq62w" + ], + "kind": "cw20", + "symbol": "SUNSET", + "name": "Sunset", + "display": "sunset", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/sunset.png" + }, + { + "denom": "juno17wzaxtfdw5em7lc94yed4ylgjme63eh73lm3lutp2rhcxttyvpwsypjm4w", + "aliases": [ + "cw20:juno17wzaxtfdw5em7lc94yed4ylgjme63eh73lm3lutp2rhcxttyvpwsypjm4w" + ], + "kind": "cw20", + "symbol": "ASVT", + "name": "Another.Software Validator Token", + "display": "asvt", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/asvt.png" + }, + { + "denom": "juno19rqljkh95gh40s7qdx40ksx3zq5tm4qsmsrdz9smw668x9zdr3lqtg33mf", + "aliases": [ + "cw20:juno19rqljkh95gh40s7qdx40ksx3zq5tm4qsmsrdz9smw668x9zdr3lqtg33mf" + ], + "kind": "cw20", + "symbol": "SEASY", + "name": "StakeEasy SEASY", + "display": "seasy", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/seasy.svg" + }, + { + "denom": "juno1a0khag6cfzu5lrwazmyndjgvlsuk7g4vn9jd8ceym8f4jf6v2l9q6d348a", + "aliases": [ + "cw20:juno1a0khag6cfzu5lrwazmyndjgvlsuk7g4vn9jd8ceym8f4jf6v2l9q6d348a" + ], + "kind": "cw20", + "symbol": "ampJUNO", + "name": "ERIS Amplified JUNO", + "display": "ampJUNO", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/ampjuno.svg" + }, + { + "denom": "juno1cltgm8v842gu54srmejewghnd6uqa26lzkpa635wzra9m9xuudkqa2gtcz", + "aliases": [ + "cw20:juno1cltgm8v842gu54srmejewghnd6uqa26lzkpa635wzra9m9xuudkqa2gtcz" + ], + "kind": "cw20", + "symbol": "FURY.legacy", + "name": "FURY.legacy", + "display": "fury", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/fanfury.png", + "trace": { + "counterpartyChainName": "furya", + "counterpartyBaseDenom": "ufury" + } + }, + { + "denom": "juno1dd0k0um5rqncfueza62w9sentdfh3ec4nw4aq4lk5hkjl63vljqscth9gv", + "aliases": [ + "cw20:juno1dd0k0um5rqncfueza62w9sentdfh3ec4nw4aq4lk5hkjl63vljqscth9gv" + ], + "kind": "cw20", + "symbol": "SEJUNO", + "name": "StakeEasy seJUNO", + "display": "sejuno", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/sejuno.svg" + }, + { + "denom": "juno1dpany8c0lj526lsa02sldv7shzvnw5dt5ues72rk35hd69rrydxqeraz8l", + "aliases": [ + "cw20:juno1dpany8c0lj526lsa02sldv7shzvnw5dt5ues72rk35hd69rrydxqeraz8l" + ], + "kind": "cw20", + "symbol": "LIGHT", + "name": "LIGHT", + "display": "light", + "decimals": 9, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/light.png" + }, + { + "denom": "juno1dtd45vxvv080v9x7hffysnmvrqm6ysecjdnvafqul28646hm04xs9gheh0", + "aliases": [ + "cw20:juno1dtd45vxvv080v9x7hffysnmvrqm6ysecjdnvafqul28646hm04xs9gheh0" + ], + "kind": "cw20", + "symbol": "HERA", + "name": "HERA", + "display": "hera", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/hera.png" + }, + { + "denom": "juno1dyyf7pxeassxvftf570krv7fdf5r8e4r04mp99h0mllsqzp3rs4q7y8yqg", + "aliases": [ + "cw20:juno1dyyf7pxeassxvftf570krv7fdf5r8e4r04mp99h0mllsqzp3rs4q7y8yqg" + ], + "kind": "cw20", + "symbol": "SPACER", + "name": "Spacer", + "display": "spacer", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/spacer.png" + }, + { + "denom": "juno1elpg96tju8a32vzn25u6asvscajjm4000589z0zthhvks28ajypqzurl7r", + "aliases": [ + "cw20:juno1elpg96tju8a32vzn25u6asvscajjm4000589z0zthhvks28ajypqzurl7r" + ], + "kind": "cw20", + "symbol": "ATEN", + "name": "ATEN", + "display": "aten", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/aten.png" + }, + { + "denom": "juno1epxnvge53c4hkcmqzlxryw5fp7eae2utyk6ehjcfpwajwp48km3sgxsh9k", + "aliases": [ + "cw20:juno1epxnvge53c4hkcmqzlxryw5fp7eae2utyk6ehjcfpwajwp48km3sgxsh9k" + ], + "kind": "cw20", + "symbol": "PEPEC", + "name": "Pepec", + "display": "pepec", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/pepec.png" + }, + { + "denom": "juno1f5datjdse3mdgrapwuzs3prl7pvxxht48ns6calnn0t77v2s9l8s0qu488", + "aliases": [ + "cw20:juno1f5datjdse3mdgrapwuzs3prl7pvxxht48ns6calnn0t77v2s9l8s0qu488" + ], + "kind": "cw20", + "symbol": "CATMOS", + "name": "Catmos", + "display": "catmos", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/catmos.png" + }, + { + "denom": "juno1g0wuyu2f49ncf94r65278puxzclf5arse9f3kvffxyv4se4vgdmsk4dvqz", + "aliases": [ + "cw20:juno1g0wuyu2f49ncf94r65278puxzclf5arse9f3kvffxyv4se4vgdmsk4dvqz" + ], + "kind": "cw20", + "symbol": "HOWL", + "name": "Howl", + "display": "howl", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/howl.png" + }, + { + "denom": "juno1g2g7ucurum66d42g8k5twk34yegdq8c82858gz0tq2fc75zy7khssgnhjl", + "aliases": [ + "cw20:juno1g2g7ucurum66d42g8k5twk34yegdq8c82858gz0tq2fc75zy7khssgnhjl" + ], + "kind": "cw20", + "symbol": "MARBLE", + "name": "Marble", + "display": "marble", + "decimals": 3, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/marble.svg" + }, + { + "denom": "juno1g647t78y2ulqlm3lss8rs3d0spzd0teuwhdvnqn92tr79yltk9dq2h24za", + "aliases": [ + "cw20:juno1g647t78y2ulqlm3lss8rs3d0spzd0teuwhdvnqn92tr79yltk9dq2h24za" + ], + "kind": "cw20", + "symbol": "RED", + "name": "Red", + "display": "red", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/red.png" + }, + { + "denom": "juno1gz8cf86zr4vw9cjcyyv432vgdaecvr9n254d3uwwkx9rermekddsxzageh", + "aliases": [ + "cw20:juno1gz8cf86zr4vw9cjcyyv432vgdaecvr9n254d3uwwkx9rermekddsxzageh" + ], + "kind": "cw20", + "symbol": "GKEY", + "name": "GKey", + "display": "gkey", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/gkey.svg" + }, + { + "denom": "juno1h86ut5aevlxuuxrra6wy3dfq6e39zkzzv9eelz678jr6amxlc4gsx46j82", + "aliases": [ + "cw20:juno1h86ut5aevlxuuxrra6wy3dfq6e39zkzzv9eelz678jr6amxlc4gsx46j82" + ], + "kind": "cw20", + "symbol": "MRVA", + "name": "MINERVA", + "display": "minerva", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/minerva.png" + }, + { + "denom": "juno1j0a9ymgngasfn3l5me8qpd53l5zlm9wurfdk7r65s5mg6tkxal3qpgf5se", + "aliases": [ + "cw20:juno1j0a9ymgngasfn3l5me8qpd53l5zlm9wurfdk7r65s5mg6tkxal3qpgf5se" + ], + "kind": "cw20", + "symbol": "GLTO", + "name": "Gelotto", + "display": "glto", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/glto.svg", + "trace": { + "counterpartyChainName": "ethereum", + "counterpartyBaseDenom": "0xd73175f9eb15eee81745d367ae59309Ca2ceb5e2" + } + }, + { + "denom": "juno1j4ux0f6gt7e82z7jdpm25v4g2gts880ap64rdwa49989wzhd0dfqed6vqm", + "aliases": [ + "cw20:juno1j4ux0f6gt7e82z7jdpm25v4g2gts880ap64rdwa49989wzhd0dfqed6vqm" + ], + "kind": "cw20", + "symbol": "SUMMIT", + "name": "Summit", + "display": "summit", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/summit.png" + }, + { + "denom": "juno1jrr0tuuzxrrwcg6hgeqhw5wqpck2y55734e7zcrp745aardlp0qqg8jz06", + "aliases": [ + "cw20:juno1jrr0tuuzxrrwcg6hgeqhw5wqpck2y55734e7zcrp745aardlp0qqg8jz06" + ], + "kind": "cw20", + "symbol": "APEMOS", + "name": "Apemos", + "display": "apemos", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/apemos.png" + }, + { + "denom": "juno1ju8k8sqwsqu5k6umrypmtyqu2wqcpnrkf4w4mntvl0javt4nma7s8lzgss", + "aliases": [ + "cw20:juno1ju8k8sqwsqu5k6umrypmtyqu2wqcpnrkf4w4mntvl0javt4nma7s8lzgss" + ], + "kind": "cw20", + "symbol": "CASA", + "name": "Casa", + "display": "casa", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/casa.png" + }, + { + "denom": "juno1jwdy7v4egw36pd84aeks3ww6n8k7zhsumd4ac8q5lts83ppxueus4626e8", + "aliases": [ + "cw20:juno1jwdy7v4egw36pd84aeks3ww6n8k7zhsumd4ac8q5lts83ppxueus4626e8" + ], + "kind": "cw20", + "symbol": "INVDRS", + "name": "Invaders", + "display": "invdrs", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/invdrs.png" + }, + { + "denom": "juno1k2ruzzvvwwtwny6gq6kcwyfhkzahaunp685wmz4hafplduekj98q9hgs6d", + "aliases": [ + "cw20:juno1k2ruzzvvwwtwny6gq6kcwyfhkzahaunp685wmz4hafplduekj98q9hgs6d" + ], + "kind": "cw20", + "symbol": "DOGA", + "name": "Doge Apr", + "display": "doga", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/doga.png" + }, + { + "denom": "juno1llg7q2d5dqlrqzh5dxv8c7kzzjszld34s5vktqmlmaaxqjssz43sxyhq0d", + "aliases": [ + "cw20:juno1llg7q2d5dqlrqzh5dxv8c7kzzjszld34s5vktqmlmaaxqjssz43sxyhq0d" + ], + "kind": "cw20", + "symbol": "MILE", + "name": "Mille", + "display": "mile", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/mille.png" + }, + { + "denom": "juno1lpvx3mv2a6ddzfjc7zzz2v2cm5gqgqf0hx67hc5p5qwn7hz4cdjsnznhu8", + "aliases": [ + "cw20:juno1lpvx3mv2a6ddzfjc7zzz2v2cm5gqgqf0hx67hc5p5qwn7hz4cdjsnznhu8" + ], + "kind": "cw20", + "symbol": "VOID", + "name": "Void", + "display": "void", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/void.png" + }, + { + "denom": "juno1m4h8q4p305wgy7vkux0w6e5ylhqll3s6pmadhxkhqtuwd5wlxhxs8xklsw", + "aliases": [ + "cw20:juno1m4h8q4p305wgy7vkux0w6e5ylhqll3s6pmadhxkhqtuwd5wlxhxs8xklsw" + ], + "kind": "cw20", + "symbol": "WATR", + "name": "WATR", + "display": "watr", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/watr.png" + }, + { + "denom": "juno1mkw83sv6c7sjdvsaplrzc8yaes9l42p4mhy0ssuxjnyzl87c9eps7ce3m9", + "aliases": [ + "cw20:juno1mkw83sv6c7sjdvsaplrzc8yaes9l42p4mhy0ssuxjnyzl87c9eps7ce3m9" + ], + "kind": "cw20", + "symbol": "WYND", + "name": "Wynd DAO Governance Token", + "display": "wynd", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/wynd.svg" + }, + { + "denom": "juno1mvkgcr5uce2rnpzr4qrzf50hx4qreuwzlt7fzsjrhjud3xnjmttq5mkh2m", + "aliases": [ + "cw20:juno1mvkgcr5uce2rnpzr4qrzf50hx4qreuwzlt7fzsjrhjud3xnjmttq5mkh2m" + ], + "kind": "cw20", + "symbol": "bJUNO", + "name": "BackBone Labs Liquid Staked JUNO", + "display": "bJUNO", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/bJUNO-bbl.png", + "coingeckoId": "backbone-labs-staked-juno", + "trace": { + "counterpartyChainName": "juno", + "counterpartyBaseDenom": "ujuno" + } + }, + { + "denom": "juno1n7n7d5088qlzlj37e9mgmkhx6dfgtvt02hqxq66lcap4dxnzdhwqfmgng3", + "aliases": [ + "cw20:juno1n7n7d5088qlzlj37e9mgmkhx6dfgtvt02hqxq66lcap4dxnzdhwqfmgng3" + ], + "kind": "cw20", + "symbol": "JOE", + "name": "JoeDAO", + "display": "joe", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/joe.png" + }, + { + "denom": "juno1ngww7zxak55fql42wmyqrr4rhzpne24hhs4p3w4cwhcdgqgr3hxsmzl9zg", + "aliases": [ + "cw20:juno1ngww7zxak55fql42wmyqrr4rhzpne24hhs4p3w4cwhcdgqgr3hxsmzl9zg" + ], + "kind": "cw20", + "symbol": "CLST", + "name": "Celestims", + "display": "clst", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/celestims.png" + }, + { + "denom": "juno1p8x807f6h222ur0vssqy3qk6mcpa40gw2pchquz5atl935t7kvyq894ne3", + "aliases": [ + "cw20:juno1p8x807f6h222ur0vssqy3qk6mcpa40gw2pchquz5atl935t7kvyq894ne3" + ], + "kind": "cw20", + "symbol": "MUSE", + "name": "MuseDAO", + "display": "muse", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/muse.png" + }, + { + "denom": "juno1qmlchtmjpvu0cr7u0tad2pq8838h6farrrjzp39eqa9xswg7teussrswlq", + "aliases": [ + "cw20:juno1qmlchtmjpvu0cr7u0tad2pq8838h6farrrjzp39eqa9xswg7teussrswlq" + ], + "kind": "cw20", + "symbol": "NRIDE", + "name": "nRide Token", + "display": "nride", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/nride.svg" + }, + { + "denom": "juno1qqwf3lkfjhp77yja7gmg3y95pda0e5xctqrdhf3wvwdd79flagvqfgrgxp", + "aliases": [ + "cw20:juno1qqwf3lkfjhp77yja7gmg3y95pda0e5xctqrdhf3wvwdd79flagvqfgrgxp" + ], + "kind": "cw20", + "symbol": "SKOJ", + "name": "Sikoba Token", + "display": "sikoba", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/sikoba.svg" + }, + { + "denom": "juno1qsrercqegvs4ye0yqg93knv73ye5dc3prqwd6jcdcuj8ggp6w0us66deup", + "aliases": [ + "cw20:juno1qsrercqegvs4ye0yqg93knv73ye5dc3prqwd6jcdcuj8ggp6w0us66deup" + ], + "kind": "cw20", + "symbol": "LOOP", + "name": "Loop Finance", + "display": "loop", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/loop.png" + }, + { + "denom": "juno1r4pzw8f9z0sypct5l9j906d47z998ulwvhvqe5xdwgy8wf84583sxwh0pa", + "aliases": [ + "cw20:juno1r4pzw8f9z0sypct5l9j906d47z998ulwvhvqe5xdwgy8wf84583sxwh0pa" + ], + "kind": "cw20", + "symbol": "RAC", + "name": "Racoon", + "display": "rac", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/rac.png", + "trace": { + "counterpartyChainName": "migaloo", + "counterpartyBaseDenom": "factory/migaloo1eqntnl6tzcj9h86psg4y4h6hh05g2h9nj8e09l/urac" + } + }, + { + "denom": "juno1re3x67ppxap48ygndmrc7har2cnc7tcxtm9nplcas4v0gc3wnmvs3s807z", + "aliases": [ + "cw20:juno1re3x67ppxap48ygndmrc7har2cnc7tcxtm9nplcas4v0gc3wnmvs3s807z" + ], + "kind": "cw20", + "symbol": "HOPE", + "name": "Hope Galaxy", + "display": "hope", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/hope.png" + }, + { + "denom": "juno1rws84uz7969aaa7pej303udhlkt3j9ca0l3egpcae98jwak9quzq8szn2l", + "aliases": [ + "cw20:juno1rws84uz7969aaa7pej303udhlkt3j9ca0l3egpcae98jwak9quzq8szn2l" + ], + "kind": "cw20", + "symbol": "PHMN.legacy", + "name": "POSTHUMAN", + "display": "phmn", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/phmn.svg", + "trace": { + "counterpartyChainName": "cosmoshub", + "counterpartyBaseDenom": "factory/cosmos146s5j3t7gh2g37ywm47dp8avhesu2htvjjaxq7z55e7xj0rq0k8q5qnjjy/PHMN" + } + }, + { + "denom": "juno1s2dp05rspeuzzpzyzdchk262szehrtfpz847uvf98cnwh53ulx4qg20qwj", + "aliases": [ + "cw20:juno1s2dp05rspeuzzpzyzdchk262szehrtfpz847uvf98cnwh53ulx4qg20qwj" + ], + "kind": "cw20", + "symbol": "BANANA", + "name": "Banana Token", + "display": "banana", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/banana.png" + }, + { + "denom": "juno1sfwye65qxcfsc837gu5qcprcz7w49gkv3wnat04764ld76hy3arqs779tr", + "aliases": [ + "cw20:juno1sfwye65qxcfsc837gu5qcprcz7w49gkv3wnat04764ld76hy3arqs779tr" + ], + "kind": "cw20", + "symbol": "DLA", + "name": "Digital Land Acquisition DAO", + "display": "dla", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/dla.png" + }, + { + "denom": "juno1spjes0smg5yp40dl7gqyw0h8rn03tnmve06dd2m5acwgh6tlx86swha3xg", + "aliases": [ + "cw20:juno1spjes0smg5yp40dl7gqyw0h8rn03tnmve06dd2m5acwgh6tlx86swha3xg" + ], + "kind": "cw20", + "symbol": "AFA", + "name": "Airdrop For All", + "display": "cw20:juno1spjes0smg5yp40dl7gqyw0h8rn03tnmve06dd2m5acwgh6tlx86swha3xg", + "decimals": 0, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/afa.png" + }, + { + "denom": "juno1t46z6hg8vvsena7sue0vg6w85ljar3cundplkre9sz0skeqkap9sxyyy6m", + "aliases": [ + "cw20:juno1t46z6hg8vvsena7sue0vg6w85ljar3cundplkre9sz0skeqkap9sxyyy6m" + ], + "kind": "cw20", + "symbol": "HOLE", + "name": "BlackHole", + "display": "hole", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/hole.png" + }, + { + "denom": "juno1tdjwrqmnztn2j3sj2ln9xnyps5hs48q3ddwjrz7jpv6mskappjys5czd49", + "aliases": [ + "cw20:juno1tdjwrqmnztn2j3sj2ln9xnyps5hs48q3ddwjrz7jpv6mskappjys5czd49" + ], + "kind": "cw20", + "symbol": "DHK", + "name": "DHK", + "display": "dhk", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/dhk.png" + }, + { + "denom": "juno1u45shlp0q4gcckvsj06ss4xuvsu0z24a0d0vr9ce6r24pht4e5xq7q995n", + "aliases": [ + "cw20:juno1u45shlp0q4gcckvsj06ss4xuvsu0z24a0d0vr9ce6r24pht4e5xq7q995n" + ], + "kind": "cw20", + "symbol": "HOPERS", + "name": "Hopers", + "display": "hopers", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/hopers.svg" + }, + { + "denom": "juno1u8cr3hcjvfkzxcaacv9q75uw9hwjmn8pucc93pmy6yvkzz79kh3qncca8x", + "aliases": [ + "cw20:juno1u8cr3hcjvfkzxcaacv9q75uw9hwjmn8pucc93pmy6yvkzz79kh3qncca8x" + ], + "kind": "cw20", + "symbol": "FOX", + "name": "Juno Fox", + "display": "fox", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/fox.png" + }, + { + "denom": "juno1ur4jx0sxchdevahep7fwq28yk4tqsrhshdtylz46yka3uf6kky5qllqp4k", + "aliases": [ + "cw20:juno1ur4jx0sxchdevahep7fwq28yk4tqsrhshdtylz46yka3uf6kky5qllqp4k" + ], + "kind": "cw20", + "symbol": "HNS", + "name": "IBC HNS (Handshake)", + "display": "hns", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/hns.svg" + }, + { + "denom": "juno1uu3rxu7w7fpfj4sl4xpxppgymk57mzdzn6kg7492jdxh5dwk7d2qq9429e", + "aliases": [ + "cw20:juno1uu3rxu7w7fpfj4sl4xpxppgymk57mzdzn6kg7492jdxh5dwk7d2qq9429e" + ], + "kind": "cw20", + "symbol": "TREE", + "name": "Living Tree", + "display": "tree", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/livingtree.png" + }, + { + "denom": "juno1wwnhkagvcd3tjz6f8vsdsw5plqnw8qy2aj3rrhqr2axvktzv9q2qz8jxn3", + "aliases": [ + "cw20:juno1wwnhkagvcd3tjz6f8vsdsw5plqnw8qy2aj3rrhqr2axvktzv9q2qz8jxn3" + ], + "kind": "cw20", + "symbol": "BJUNO", + "name": "StakeEasy bJUNO", + "display": "bjuno", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/bjuno.svg" + }, + { + "denom": "juno1x5qt47rw84c4k6xvvywtrd40p8gxjt8wnmlahlqg07qevah3f8lqwxfs7z", + "aliases": [ + "cw20:juno1x5qt47rw84c4k6xvvywtrd40p8gxjt8wnmlahlqg07qevah3f8lqwxfs7z" + ], + "kind": "cw20", + "symbol": "SHIBAC", + "name": "ShibaCosmos", + "display": "shibac", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/shibacosmos.png" + }, + { + "denom": "juno1xekkh27punj0uxruv3gvuydyt856fax0nu750xns99t2qcxp7xmsqwhfma", + "aliases": [ + "cw20:juno1xekkh27punj0uxruv3gvuydyt856fax0nu750xns99t2qcxp7xmsqwhfma" + ], + "kind": "cw20", + "symbol": "GRDN", + "name": "Guardian", + "display": "grdn", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/guardian.png" + }, + { + "denom": "juno1y9rf7ql6ffwkv02hsgd4yruz23pn4w97p75e2slsnkm0mnamhzysvqnxaq", + "aliases": [ + "cw20:juno1y9rf7ql6ffwkv02hsgd4yruz23pn4w97p75e2slsnkm0mnamhzysvqnxaq" + ], + "kind": "cw20", + "symbol": "BLOCK", + "name": "Block", + "display": "block", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/block.svg" + }, + { + "denom": "juno1ytymtllllsp3hfmndvcp802p2xmy5s8m59ufel8xv9ahyxyfs4hs4kd4je", + "aliases": [ + "cw20:juno1ytymtllllsp3hfmndvcp802p2xmy5s8m59ufel8xv9ahyxyfs4hs4kd4je" + ], + "kind": "cw20", + "symbol": "OSDOGE", + "name": "Osmosis Doge", + "display": "osdoge", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/osdoge.png" + }, + { + "denom": "juno1zkwveux7y6fmsr88atf3cyffx96p0c96qr8tgcsj7vfnhx7sal3s3zu3ps", + "aliases": [ + "cw20:juno1zkwveux7y6fmsr88atf3cyffx96p0c96qr8tgcsj7vfnhx7sal3s3zu3ps" + ], + "kind": "cw20", + "symbol": "JAPE", + "name": "Junø Apes", + "display": "jape", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/jape.png" + }, + { + "denom": "juno1zqrj3ta4u7ylv0wqzd8t8q3jrr9rdmn43zuzp9zemeunecnhy8fss778g7", + "aliases": [ + "cw20:juno1zqrj3ta4u7ylv0wqzd8t8q3jrr9rdmn43zuzp9zemeunecnhy8fss778g7" + ], + "kind": "cw20", + "symbol": "PEPE", + "name": "Osmo Pepe", + "display": "pepe", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/pepe.png" + }, + { + "denom": "ujuno", + "kind": "native", + "symbol": "JUNO", + "name": "Juno", + "display": "juno", + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/juno.svg", + "coingeckoId": "juno-network" + } + ] +} diff --git a/frontend/src/data/registry.juno-1.json b/frontend/src/data/registry.juno-1.json new file mode 100644 index 000000000..d033e98b5 --- /dev/null +++ b/frontend/src/data/registry.juno-1.json @@ -0,0 +1,175 @@ +{ + "chainId": "juno-1", + "chainName": "Juno", + "rpcEndpoint": "https://juno-rpc.kleomedes.network", + "restEndpoint": "https://juno-api.polkachu.com", + "explorerBaseUrl": "https://ping.pub/juno", + "factory": "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca", + "nativeCoinRegistry": "juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2", + "router": "juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s", + "incentives": "juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598", + "oracle": "juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p", + "updatedAt": "2026-07-13T00:00:00Z", + "pools": [ + { + "id": "juno-agent-preview-xyk-1", + "label": "JUNO / Juno Agent Test", + "pair": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "lpToken": "factory/juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv/astroport/share", + "type": "xyk", + "feeBps": 30, + "assets": [ + { + "kind": "native", + "id": "ujuno", + "symbol": "JUNO", + "decimals": 6, + "verified": true, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/juno.svg" + }, + { + "kind": "native", + "id": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + "symbol": "JUNOAGENT-TEST", + "decimals": 6, + "verified": true + } + ], + "explorer": "https://ping.pub/juno/wasm/contract/juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "enabled": true, + "status": "active", + "verified": true, + "featured": false, + "notes": "Preview/test TokenFactory pool from PR #9. Treat as active thin-liquidity infrastructure, not a public launch market." + }, + { + "id": "season0-twolf-juno", + "label": "TWOLF / JUNO", + "pair": "juno1cpvfjvx96eqj7lnksk7e83kpy8nmpnnz55ukzw6vg9c3ymt9j8gscr5lk9", + "lpToken": "factory/juno1cpvfjvx96eqj7lnksk7e83kpy8nmpnnz55ukzw6vg9c3ymt9j8gscr5lk9/astroport/share", + "type": "xyk", + "feeBps": 30, + "assets": [ + { + "kind": "native", + "id": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/twolf", + "symbol": "TWOLF", + "name": "Juno Meme Season 0 Test Wolf", + "display": "twolf", + "decimals": 6, + "verified": true + }, + { + "kind": "native", + "id": "ujuno", + "symbol": "JUNO", + "decimals": 6, + "verified": true, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/juno.svg" + } + ], + "explorer": "https://ping.pub/juno/wasm/contract/juno1cpvfjvx96eqj7lnksk7e83kpy8nmpnnz55ukzw6vg9c3ymt9j8gscr5lk9", + "enabled": true, + "status": "active", + "verified": true, + "notes": "Juno Meme Season 0 disposable test-token pool on juno-1; thin liquidity, not a production launch market." + }, + { + "id": "season0-traw-juno", + "label": "TRAW / JUNO", + "pair": "juno193yw4e925qdkh3pv6k2uw2m9gq0kx2p5760kup26ryw45pdzkcfqdv2nnm", + "lpToken": "factory/juno193yw4e925qdkh3pv6k2uw2m9gq0kx2p5760kup26ryw45pdzkcfqdv2nnm/astroport/share", + "type": "xyk", + "feeBps": 30, + "assets": [ + { + "kind": "native", + "id": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/traw", + "symbol": "TRAW", + "name": "Juno Meme Season 0 Test RAW", + "display": "traw", + "decimals": 6, + "verified": true + }, + { + "kind": "native", + "id": "ujuno", + "symbol": "JUNO", + "decimals": 6, + "verified": true, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/juno.svg" + } + ], + "explorer": "https://ping.pub/juno/wasm/contract/juno193yw4e925qdkh3pv6k2uw2m9gq0kx2p5760kup26ryw45pdzkcfqdv2nnm", + "enabled": true, + "status": "active", + "verified": true, + "notes": "Juno Meme Season 0 disposable test-token pool on juno-1; thin liquidity, not a production launch market." + }, + { + "id": "season0-tahab-juno", + "label": "TAHAB / JUNO", + "pair": "juno1fckcyn7ww8tmj4p60qlfv4y7dxzlnj3hk22nhafm9875lcll5cmqvwnm77", + "lpToken": "factory/juno1fckcyn7ww8tmj4p60qlfv4y7dxzlnj3hk22nhafm9875lcll5cmqvwnm77/astroport/share", + "type": "xyk", + "feeBps": 30, + "assets": [ + { + "kind": "native", + "id": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/tahab", + "symbol": "TAHAB", + "name": "Juno Meme Season 0 Test AHAB", + "display": "tahab", + "decimals": 6, + "verified": true + }, + { + "kind": "native", + "id": "ujuno", + "symbol": "JUNO", + "decimals": 6, + "verified": true, + "logoURI": "https://raw.githubusercontent.com/cosmos/chain-registry/master/juno/images/juno.svg" + } + ], + "explorer": "https://ping.pub/juno/wasm/contract/juno1fckcyn7ww8tmj4p60qlfv4y7dxzlnj3hk22nhafm9875lcll5cmqvwnm77", + "enabled": true, + "status": "active", + "verified": true, + "notes": "Juno Meme Season 0 disposable test-token pool on juno-1; thin liquidity, not a production launch market." + }, + { + "id": "season0-tahab-tfud", + "label": "TAHAB / TFUD", + "pair": "juno1mqkzf02hjlkzcnjn0yv9lr6d9j3tmx65qwfs0hcxy5ul4pfequ4s8n9hnn", + "lpToken": "factory/juno1mqkzf02hjlkzcnjn0yv9lr6d9j3tmx65qwfs0hcxy5ul4pfequ4s8n9hnn/astroport/share", + "type": "xyk", + "feeBps": 30, + "assets": [ + { + "kind": "native", + "id": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/tahab", + "symbol": "TAHAB", + "name": "Juno Meme Season 0 Test AHAB", + "display": "tahab", + "decimals": 6, + "verified": true + }, + { + "kind": "native", + "id": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/tfud", + "symbol": "TFUD", + "name": "Juno Meme Season 0 Test FUD", + "display": "tfud", + "decimals": 6, + "verified": true + } + ], + "explorer": "https://ping.pub/juno/wasm/contract/juno1mqkzf02hjlkzcnjn0yv9lr6d9j3tmx65qwfs0hcxy5ul4pfequ4s8n9hnn", + "enabled": true, + "status": "active", + "verified": true, + "notes": "Juno Meme Season 0 disposable test-token pool on juno-1; thin liquidity, not a production launch market." + } + ] +} diff --git a/frontend/src/e2e/mocks.ts b/frontend/src/e2e/mocks.ts new file mode 100644 index 000000000..5c3ae6a2d --- /dev/null +++ b/frontend/src/e2e/mocks.ts @@ -0,0 +1,146 @@ +import type { RegistryPool } from "../config/registry"; +import type { PoolResponse, SimulationResponse, ReverseSimulationResponse, SimulateSwapOperationsResponse } from "../lib/astroport/queries"; +import type { WalletBalance } from "../queries/useWalletBalances"; + +export const E2E_WALLET_ADDRESS = "juno1e2etestwallet0000000000000000000000000000000000"; +export const E2E_TX_HASH_PREFIX = "E2E_MOCK_TX_"; + +type ExecuteCall = { + sender: string; + contractAddress: string; + msg: unknown; + fee: unknown; + memo?: string; + funds?: unknown; +}; + +type E2EWindow = Window & typeof globalThis & { + __DEX_E2E_TXS__?: ExecuteCall[]; + __DEX_E2E_TX_COUNT__?: number; + __DEX_E2E_TX_MODE__?: "success" | "reject" | "fail" | "timeout" | "delay"; + __DEX_E2E_RELEASE_TX__?: () => void; +}; + +export function isE2EMode() { + return import.meta.env.VITE_DEX_E2E === "true"; +} + +function txHashFor(label: string) { + const win = window as E2EWindow; + win.__DEX_E2E_TX_COUNT__ = (win.__DEX_E2E_TX_COUNT__ ?? 0) + 1; + return `${E2E_TX_HASH_PREFIX}${label}_${String(win.__DEX_E2E_TX_COUNT__).padStart(3, "0")}`; +} + +function recordExecute(call: ExecuteCall) { + const win = window as E2EWindow; + win.__DEX_E2E_TXS__ = [...(win.__DEX_E2E_TXS__ ?? []), call]; +} + +function labelForMsg(msg: unknown) { + if (msg && typeof msg === "object") { + if ("swap" in msg || "execute_swap_operations" in msg) return "SWAP"; + if ("provide_liquidity" in msg) return "PROVIDE"; + if ("withdraw_liquidity" in msg || "send" in msg) return "WITHDRAW"; + if ("create_pair" in msg) return "CREATE_PAIR"; + if ("deposit" in msg) return "STAKE"; + if ("withdraw" in msg) return "UNSTAKE"; + if ("claim_rewards" in msg) return "CLAIM"; + } + return "BROADCAST"; +} + +export function createE2ESigningClient() { + return { + simulate: async () => 100_000, + execute: async (sender: string, contractAddress: string, msg: unknown, fee: unknown, memo?: string, funds?: unknown) => { + const win = window as E2EWindow; + if (win.__DEX_E2E_TX_MODE__ === "reject") throw new Error("User rejected the signature request"); + if (win.__DEX_E2E_TX_MODE__ === "fail") throw new Error("Broadcast failed before a transaction hash was returned"); + recordExecute({ sender, contractAddress, msg, fee, memo, funds }); + if (win.__DEX_E2E_TX_MODE__ === "timeout") throw new Error("Transaction not found after broadcast timeout"); + if (win.__DEX_E2E_TX_MODE__ === "delay") { + await new Promise((resolve) => { win.__DEX_E2E_RELEASE_TX__ = resolve; }); + win.__DEX_E2E_RELEASE_TX__ = undefined; + } + return { + transactionHash: txHashFor(labelForMsg(msg)), + height: 123456, + gasWanted: 180000, + gasUsed: 125000, + logs: [], + events: [], + pairAddress: "juno1e2ecreatedpair00000000000000000000000000000000", + }; + }, + }; +} + +export function e2ePoolResponse(pool: RegistryPool): PoolResponse { + return { + assets: pool.assets.map((asset, index) => ({ + info: asset.kind === "cw20" ? { token: { contract_addr: asset.id } } : { native_token: { denom: asset.id } }, + amount: index === 0 ? "100000000000" : "200000000000", + })), + total_share: "100000000000", + } as PoolResponse; +} + +export function e2eSwapSimulation(amount: string): SimulationResponse { + const offer = BigInt(amount || "0"); + const returnAmount = (offer * 197n) / 100n; + return { + return_amount: returnAmount.toString(), + spread_amount: (offer / 1000n).toString(), + commission_amount: (offer / 300n).toString(), + } as SimulationResponse; +} + +export function e2eReverseSwapSimulation(amount: string): ReverseSimulationResponse { + const ask = BigInt(amount || "0"); + return { + offer_amount: ((ask * 103n) / 200n).toString(), + spread_amount: (ask / 1000n).toString(), + commission_amount: (ask / 300n).toString(), + } as ReverseSimulationResponse; +} + +export function e2eRouterSimulation(amount: string): SimulateSwapOperationsResponse { + return { amount: ((BigInt(amount || "0") * 197n) / 100n).toString() } as SimulateSwapOperationsResponse; +} + +export function e2eBalances(pools: RegistryPool[]): WalletBalance[] { + return pools.flatMap((pool) => [ + { + denom: pool.assets[0].id, + symbol: pool.assets[0].symbol, + decimals: pool.assets[0].decimals, + name: pool.assets[0].name, + source: "registry" as const, + poolId: pool.id, + poolLabel: pool.label, + amount: "500000000000", + isKnownDenom: true, + }, + { + denom: pool.assets[1].id, + symbol: pool.assets[1].symbol, + decimals: pool.assets[1].decimals, + name: pool.assets[1].name, + source: "registry" as const, + poolId: pool.id, + poolLabel: pool.label, + amount: "500000000000", + isKnownDenom: true, + }, + { + denom: pool.lpToken, + symbol: `${pool.assets[0].symbol}/${pool.assets[1].symbol} LP`, + decimals: 6, + source: "lp" as const, + poolId: pool.id, + poolLabel: pool.label, + amount: "25000000000", + isKnownDenom: true, + }, + ]); +} diff --git a/frontend/src/integration/exposedAssetExecution.test.ts b/frontend/src/integration/exposedAssetExecution.test.ts new file mode 100644 index 000000000..f646a14a5 --- /dev/null +++ b/frontend/src/integration/exposedAssetExecution.test.ts @@ -0,0 +1,142 @@ +import { fromBase64, fromUtf8 } from "@cosmjs/encoding"; +import { describe, expect, it } from "vitest"; +import type { RegistryAsset, RegistryPool } from "../config/registry"; +import { dexRegistry } from "../config/registry"; +import { routeToOperations, type SwapRoute } from "../lib/astroport/routes"; +import { executeInstructionToEncodeObject } from "../lib/cosmjs/fees"; +import { buildCreatePoolExecuteInstruction } from "../mutations/useCreatePoolTx"; +import { buildProvideLiquidityExecuteInstruction } from "../mutations/useProvideLiquidityTx"; +import { buildSwapExecuteInstruction } from "../mutations/useSwapTx"; +import { buildWithdrawLiquidityExecuteInstruction } from "../mutations/useWithdrawLiquidityTx"; + +const sender = "juno1sender000000000000000000000000000000000"; +const native: RegistryAsset = { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, verified: true }; +const ibc: RegistryAsset = { kind: "ibc", id: "ibc/0123456789ABCDEF", symbol: "USDC", decimals: 6, verified: true, denomTrace: "transfer/channel-1/uusdc" }; +const tokenFactory: RegistryAsset = { kind: "native", id: "factory/juno1issuer000000000000000000000000000000/asset", symbol: "FACT", decimals: 6, verified: true }; +const cw20: RegistryAsset = { kind: "cw20", id: "juno1cw20token000000000000000000000000000000000", symbol: "CW20", decimals: 6, verified: true }; +const bridge: RegistryAsset = { kind: "native", id: "uatom", symbol: "ATOM", decimals: 6, verified: true }; + +function pool(id: string, assets: [RegistryAsset, RegistryAsset]): RegistryPool { + return { + id, + label: assets.map((asset) => asset.symbol).join(" / "), + pair: `juno1${id}pair000000000000000000000000000000000`, + lpToken: `factory/juno1${id}pair000000000000000000000000000000000/astroport/share`, + explorer: `https://explorer.invalid/${id}`, + type: "xyk", + feeBps: 30, + assets, + enabled: true, + status: "active", + verified: true, + source: "registry", + }; +} + +function directRoute(offer: RegistryAsset, ask = bridge): SwapRoute { + const pair = pool(`direct${offer.symbol.toLowerCase()}`, [offer, ask]); + const hops = [{ pool: pair, offerAsset: offer, askAsset: ask }]; + return { id: pair.id, hops, operations: routeToOperations(hops) }; +} + +function routedRoute(offer: RegistryAsset, ask = native): SwapRoute { + const first = pool(`first${offer.symbol.toLowerCase()}`, [offer, bridge]); + const second = pool(`second${offer.symbol.toLowerCase()}`, [bridge, ask]); + const hops = [ + { pool: first, offerAsset: offer, askAsset: bridge }, + { pool: second, offerAsset: bridge, askAsset: ask }, + ]; + return { id: `${first.id}|${second.id}`, hops, operations: routeToOperations(hops) }; +} + +function encodedJson(instruction: ReturnType) { + const encoded = executeInstructionToEncodeObject(sender, instruction); + return JSON.parse(fromUtf8(encoded.value.msg!)) as Record; +} + +describe("exposed asset execution integration matrix", () => { + it.each([ + ["native", native], + ["IBC", ibc], + ["TokenFactory", tokenFactory], + ])("encodes a bounded direct %s swap with the exact offered funds", (_label, offer) => { + const route = directRoute(offer); + const instruction = buildSwapExecuteInstruction({ route, pool: route.hops[0].pool, offerAsset: offer, askAsset: bridge, amount: "1000000", maxSpread: "0.01", minimumReceive: "900000", source: "pair" }); + + expect(instruction.contractAddress).toBe(route.hops[0].pool.pair); + expect(instruction.funds).toEqual([{ denom: offer.id, amount: "1000000" }]); + expect(encodedJson(instruction)).toMatchObject({ swap: { offer_asset: { amount: "1000000" }, max_spread: "0.01" } }); + }); + + it("encodes a direct CW20 swap as one atomic send hook", () => { + const route = directRoute(cw20); + const instruction = buildSwapExecuteInstruction({ route, pool: route.hops[0].pool, offerAsset: cw20, askAsset: bridge, amount: "1000000", maxSpread: "0.01", minimumReceive: "900000", source: "pair" }); + const message = encodedJson(instruction) as { send: { contract: string; amount: string; msg: string } }; + + expect(instruction.contractAddress).toBe(cw20.id); + expect(instruction.funds ?? []).toEqual([]); + expect(message.send).toMatchObject({ contract: route.hops[0].pool.pair, amount: "1000000" }); + expect(JSON.parse(fromUtf8(fromBase64(message.send.msg)))).toMatchObject({ swap: { max_spread: "0.01" } }); + }); + + it.each([ + ["native", native], + ["IBC", ibc], + ["TokenFactory", tokenFactory], + ])("encodes a bounded multi-hop %s router swap with exact funds", (_label, offer) => { + const route = routedRoute(offer); + const instruction = buildSwapExecuteInstruction({ route, offerAsset: offer, askAsset: native, amount: "1000000", maxSpread: "0.01", minimumReceive: "900000", source: "router" }); + + expect(instruction.contractAddress).toBe(dexRegistry.router); + expect(instruction.funds).toEqual([{ denom: offer.id, amount: "1000000" }]); + expect(encodedJson(instruction)).toMatchObject({ execute_swap_operations: { minimum_receive: "900000", max_spread: "0.01" } }); + expect((encodedJson(instruction) as any).execute_swap_operations.operations).toHaveLength(2); + }); + + it("encodes a multi-hop CW20 router swap as one bounded atomic send hook", () => { + const route = routedRoute(cw20); + const instruction = buildSwapExecuteInstruction({ route, offerAsset: cw20, askAsset: native, amount: "1000000", maxSpread: "0.01", minimumReceive: "900000", source: "router" }); + const message = encodedJson(instruction) as { send: { contract: string; amount: string; msg: string } }; + const hook = JSON.parse(fromUtf8(fromBase64(message.send.msg))); + + expect(instruction.contractAddress).toBe(cw20.id); + expect(message.send).toMatchObject({ contract: dexRegistry.router, amount: "1000000" }); + expect(hook.execute_swap_operations).toMatchObject({ minimum_receive: "900000", max_spread: "0.01" }); + expect(hook.execute_swap_operations.operations).toHaveLength(2); + }); + + it.each([ + ["native + IBC", native, ibc], + ["native + TokenFactory", native, tokenFactory], + ["IBC + TokenFactory", ibc, tokenFactory], + ])("encodes bounded provide and withdraw paths for %s liquidity", (_label, first, second) => { + const target = pool(`liquidity${first.symbol.toLowerCase()}${second.symbol.toLowerCase()}`, [first, second]); + const provide = buildProvideLiquidityExecuteInstruction({ pool: target, amounts: ["1000000", "2000000"], slippageTolerance: "0.01", minLpToReceive: "950000" }); + const withdraw = buildWithdrawLiquidityExecuteInstruction({ pool: target, lpAmount: "500000", minAssetsToReceive: [ + { info: { native_token: { denom: first.id } }, amount: "450000" }, + { info: { native_token: { denom: second.id } }, amount: "900000" }, + ] }); + + expect(provide.funds).toHaveLength(2); + expect((provide.msg as any).provide_liquidity).toMatchObject({ slippage_tolerance: "0.01", min_lp_to_receive: "950000" }); + expect(withdraw.funds).toEqual([{ denom: target.lpToken, amount: "500000" }]); + expect((withdraw.msg as any).withdraw_liquidity.min_assets_to_receive).toHaveLength(2); + }); + + it("rejects the unexposed CW20 liquidity path at the execution boundary", () => { + expect(() => buildProvideLiquidityExecuteInstruction({ pool: pool("cw20liquidity", [native, cw20]), amounts: ["1", "1"] })).toThrow(/exact allowances/i); + }); + + it.each([ + ["native", native], + ["IBC", ibc], + ["TokenFactory", tokenFactory], + ["CW20", cw20], + ])("encodes %s asset identity for permissionless pool creation", (_label, asset) => { + const instruction = buildCreatePoolExecuteInstruction({ assets: [native, asset === native ? bridge : asset], option: { id: "xyk", label: "XYK", pairType: { xyk: {} } } }); + const infos = (instruction.msg as any).create_pair.asset_infos; + expect(instruction.contractAddress).toBe(dexRegistry.factory); + expect(infos).toHaveLength(2); + expect(JSON.stringify(infos)).toContain(asset === native ? bridge.id : asset.id); + }); +}); diff --git a/frontend/src/lib/assets/assetMetadata.test.ts b/frontend/src/lib/assets/assetMetadata.test.ts new file mode 100644 index 000000000..4699121d2 --- /dev/null +++ b/frontend/src/lib/assets/assetMetadata.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { parseDexRegistry } from "../../config/registry"; +import { getChainRegistryAsset, mergeAssetMetadata, resolveAssetMetadata } from "./assetMetadata"; + +describe("chain-registry asset metadata", () => { + it("resolves JUNO metadata with decimals, name, and logo", () => { + const juno = resolveAssetMetadata("ujuno"); + + expect(juno.source).toBe("chain-registry"); + expect(juno.symbol).toBe("JUNO"); + expect(juno.name).toBe("Juno"); + expect(juno.decimals).toBe(6); + expect(juno.logoURI).toMatch(/juno\.(svg|png)$/); + }); + + it("resolves IBC denom trace metadata and counterparty hints", () => { + const atom = resolveAssetMetadata("ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9"); + + expect(atom.kind).toBe("ibc"); + expect(atom.symbol).toBe("ATOM"); + expect(atom.denomTrace).toBe("transfer/channel-1/uatom"); + expect(atom.trace?.counterpartyChainName).toBe("cosmoshub"); + expect(atom.trace?.counterpartyBaseDenom).toBe("uatom"); + }); + + it("falls back safely for unknown IBC denoms without inventing trace metadata", () => { + const unknown = resolveAssetMetadata("ibc/0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"); + + expect(unknown.source).toBe("fallback"); + expect(unknown.kind).toBe("ibc"); + expect(unknown.name).toBe("Unknown IBC asset"); + expect(unknown.decimals).toBe(6); + expect(unknown.denomTrace).toBeUndefined(); + }); + + it("merges metadata into curated assets without weakening registry validation", () => { + const curated = mergeAssetMetadata({ kind: "native", id: "ujuno", symbol: "CURATED-JUNO", decimals: 6 }); + + expect(curated.symbol).toBe("CURATED-JUNO"); + expect(curated.logoURI).toBe(getChainRegistryAsset("ujuno")?.logoURI); + expect(curated.name).toBe("Juno"); + expect(() => parseDexRegistry({ chainId: "juno-1", pools: [] })).toThrow(/chainName/); + }); +}); diff --git a/frontend/src/lib/assets/assetMetadata.ts b/frontend/src/lib/assets/assetMetadata.ts new file mode 100644 index 000000000..b573e6788 --- /dev/null +++ b/frontend/src/lib/assets/assetMetadata.ts @@ -0,0 +1,131 @@ +import chainRegistryJson from "../../data/chain-registry-assets.juno-1.json"; +import type { RegistryAsset } from "../../config/registry"; + +export type ChainRegistryAsset = { + denom: string; + aliases?: string[]; + kind: "native" | "ibc" | "cw20" | "factory"; + symbol: string; + name?: string; + display?: string; + decimals: number; + logoURI?: string; + coingeckoId?: string; + denomTrace?: string; + trace?: { + path?: string; + channelId?: string; + counterpartyChainName?: string; + counterpartyBaseDenom?: string; + counterpartyChannelId?: string; + }; +}; + +type ChainRegistryAssetList = { + chainId: "juno-1"; + source: string; + generatedAt: string; + assets: ChainRegistryAsset[]; +}; + +export type ResolvedAssetMetadata = RegistryAsset & { + name?: string; + display?: string; + coingeckoId?: string; + trace?: ChainRegistryAsset["trace"]; + source: "chain-registry" | "fallback"; +}; + +export const DEFAULT_DECIMALS = 6; +const IBC_HASH_PATTERN = /^ibc\/[0-9A-Fa-f]{64}$/; +const assetList = chainRegistryJson as ChainRegistryAssetList; + +function normalizeKind(kind: ChainRegistryAsset["kind"], denom: string): RegistryAsset["kind"] { + if (kind === "cw20") return "cw20"; + if (kind === "ibc" || denom.startsWith("ibc/")) return "ibc"; + return "native"; +} + +function fallbackSymbol(denom: string): string { + if (denom === "ujuno") return "JUNO"; + if (IBC_HASH_PATTERN.test(denom)) return `${denom.slice(0, 12)}…${denom.slice(-6)}`; + const tail = denom.split("/").filter(Boolean).at(-1) ?? denom; + return tail.length > 18 ? `${tail.slice(0, 8)}…${tail.slice(-6)}` : tail.toUpperCase(); +} + +function fallbackName(denom: string): string | undefined { + if (denom.startsWith("ibc/")) return "Unknown IBC asset"; + if (denom.startsWith("factory/")) return "TokenFactory asset"; + return undefined; +} + +function fromChainRegistry(asset: ChainRegistryAsset): ResolvedAssetMetadata { + return { + kind: normalizeKind(asset.kind, asset.denom), + id: asset.denom, + symbol: asset.symbol, + name: asset.name, + display: asset.display, + decimals: asset.decimals, + logoURI: asset.logoURI, + denomTrace: asset.denomTrace, + coingeckoId: asset.coingeckoId, + trace: asset.trace, + source: "chain-registry", + }; +} + +const metadataByDenom = new Map(); +for (const asset of assetList.assets) { + const resolved = fromChainRegistry(asset); + metadataByDenom.set(asset.denom, resolved); + for (const alias of asset.aliases ?? []) metadataByDenom.set(alias, resolved); +} + +export function getChainRegistryAsset(denom: string): ResolvedAssetMetadata | undefined { + return metadataByDenom.get(denom); +} + +export function resolveAssetMetadata(denom: string, overrides: Partial = {}): ResolvedAssetMetadata { + const base = metadataByDenom.get(denom); + const resolved: ResolvedAssetMetadata = base + ? { ...base, id: denom } + : { + kind: denom.startsWith("ibc/") ? "ibc" : denom.match(/^juno1[ac-hj-np-z02-9]{38,58}$/) ? "cw20" : "native", + id: denom, + symbol: fallbackSymbol(denom), + name: fallbackName(denom), + decimals: DEFAULT_DECIMALS, + source: "fallback", + }; + + return { + ...resolved, + ...overrides, + id: overrides.id ?? denom, + kind: overrides.kind ?? resolved.kind, + symbol: overrides.symbol ?? resolved.symbol, + decimals: overrides.decimals ?? resolved.decimals, + denomTrace: overrides.denomTrace ?? resolved.denomTrace, + logoURI: overrides.logoURI ?? resolved.logoURI, + }; +} + +export function mergeAssetMetadata(asset: RegistryAsset): RegistryAsset { + const metadata = resolveAssetMetadata(asset.id, asset); + const { source: _source, ...metadataFields } = metadata; + return { + ...metadataFields, + ...asset, + name: asset.name ?? metadata.name, + display: asset.display ?? metadata.display, + logoURI: asset.logoURI ?? metadata.logoURI, + denomTrace: asset.denomTrace ?? metadata.denomTrace, + coingeckoId: asset.coingeckoId ?? metadata.coingeckoId, + trace: asset.trace ?? metadata.trace, + }; +} + +export function getChainRegistryAssets(): ResolvedAssetMetadata[] { + return Array.from(new Map(assetList.assets.map((asset) => [asset.denom, fromChainRegistry(asset)])).values()); +} diff --git a/frontend/src/lib/astroport/assetInfo.ts b/frontend/src/lib/astroport/assetInfo.ts new file mode 100644 index 000000000..a533b07a8 --- /dev/null +++ b/frontend/src/lib/astroport/assetInfo.ts @@ -0,0 +1,40 @@ +import type { RegistryAsset } from "../../config/registry"; + +export type NativeFund = { + denom: string; + amount: string; +}; + +export type DexAssetInfo = + | { native_token: { denom: string } } + | { token: { contract_addr: string } }; + +export type DexAsset = { + info: DexAssetInfo; + amount: string; +}; + +export function toAssetInfo(asset: RegistryAsset): DexAssetInfo { + if (asset.kind === "cw20") return { token: { contract_addr: asset.id } }; + return { native_token: { denom: asset.id } }; +} + +export function toAsset(asset: RegistryAsset, amount: string): DexAsset { + return { info: toAssetInfo(asset), amount }; +} + +export function assetLabel(asset: RegistryAsset): string { + return `${asset.symbol} (${asset.id})`; +} + +export function nativeFunds(asset: RegistryAsset, amount: string) { + return asset.kind === "cw20" ? [] : [{ denom: asset.id, amount }]; +} + +export function sortNativeFunds(funds: NativeFund[]): NativeFund[] { + return [...funds].sort((left, right) => { + if (left.denom < right.denom) return -1; + if (left.denom > right.denom) return 1; + return 0; + }); +} diff --git a/frontend/src/lib/astroport/messages.test.ts b/frontend/src/lib/astroport/messages.test.ts new file mode 100644 index 000000000..201acab1e --- /dev/null +++ b/frontend/src/lib/astroport/messages.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryAsset } from "../../config/registry"; +import { fromBase64, fromUtf8 } from "@cosmjs/encoding"; +import { createCw20SwapSendMessage, createProvideLiquidityMessage, createSwapMessage } from "./messages"; + +const juno: RegistryAsset = { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6 }; +const testToken: RegistryAsset = { kind: "ibc", id: "ibc/test", symbol: "TEST", decimals: 6 }; +const factoryToken: RegistryAsset = { + kind: "native", + id: "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + symbol: "AGENT", + decimals: 6, +}; +const cw20: RegistryAsset = { kind: "cw20", id: "juno1cw20token000000000000000000000000000000000", symbol: "CW20", decimals: 6 }; + +describe("createSwapMessage", () => { + it("builds a direct pair swap payload with native funds and max spread", () => { + const payload = createSwapMessage(juno, testToken, "1000000", "0.005"); + + expect(payload).toEqual({ + msg: { + swap: { + offer_asset: { info: { native_token: { denom: "ujuno" } }, amount: "1000000" }, + ask_asset_info: { native_token: { denom: "ibc/test" } }, + max_spread: "0.005", + }, + }, + funds: [{ denom: "ujuno", amount: "1000000" }], + }); + }); + + it("uses an atomic CW20 send hook instead of an unfunded pair execute", () => { + const msg = createCw20SwapSendMessage("juno1pair", testToken, "1000000", "0.005"); + expect(msg.send).toMatchObject({ contract: "juno1pair", amount: "1000000" }); + expect(JSON.parse(fromUtf8(fromBase64(msg.send.msg)))).toEqual({ + swap: { ask_asset_info: { native_token: { denom: "ibc/test" } }, max_spread: "0.005" }, + }); + }); +}); + +describe("createProvideLiquidityMessage", () => { + it("keeps pair asset order in the message but sorts native funds by denom", () => { + const payload = createProvideLiquidityMessage([juno, factoryToken], ["1000000", "2000000"], "0.005", "995000"); + + expect(payload.msg.provide_liquidity.assets).toEqual([ + { info: { native_token: { denom: "ujuno" } }, amount: "1000000" }, + { info: { native_token: { denom: factoryToken.id } }, amount: "2000000" }, + ]); + expect(payload.funds).toEqual([ + { denom: factoryToken.id, amount: "2000000" }, + { denom: "ujuno", amount: "1000000" }, + ]); + }); + + it("hard-blocks CW20 deposits until exact allowance execution is implemented", () => { + expect(() => createProvideLiquidityMessage([juno, cw20], ["1", "1"])).toThrow(/exact allowances/i); + }); +}); diff --git a/frontend/src/lib/astroport/messages.ts b/frontend/src/lib/astroport/messages.ts new file mode 100644 index 000000000..1bce4313e --- /dev/null +++ b/frontend/src/lib/astroport/messages.ts @@ -0,0 +1,61 @@ +import type { RegistryAsset } from "../../config/registry"; +import type { Asset, ExecuteMsg as PairExecuteMsg } from "../generated/Pair.types"; +import { nativeFunds, sortNativeFunds, toAsset } from "./assetInfo"; +import { toBase64, toUtf8 } from "@cosmjs/encoding"; + +export function createSwapMessage(offerAsset: RegistryAsset, askAsset: RegistryAsset, amount: string, maxSpread: string) { + const msg = { + swap: { + offer_asset: toAsset(offerAsset, amount), + ask_asset_info: askAsset.kind === "cw20" + ? { token: { contract_addr: askAsset.id } } + : { native_token: { denom: askAsset.id } }, + max_spread: maxSpread, + }, + } satisfies PairExecuteMsg; + + return { + msg, + funds: nativeFunds(offerAsset, amount), + }; +} + +export function createCw20SwapSendMessage(pairContract: string, askAsset: RegistryAsset, amount: string, maxSpread: string) { + const hook = { + swap: { + ask_asset_info: askAsset.kind === "cw20" ? { token: { contract_addr: askAsset.id } } : { native_token: { denom: askAsset.id } }, + max_spread: maxSpread, + }, + }; + return { + send: { + contract: pairContract, + amount, + msg: toBase64(toUtf8(JSON.stringify(hook))), + }, + }; +} + +export function createProvideLiquidityMessage(assets: [RegistryAsset, RegistryAsset], amounts: [string, string], slippageTolerance = "0.01", minLpToReceive?: string) { + if (assets.some((asset) => asset.kind === "cw20")) throw new Error("CW20 add liquidity is unavailable until exact allowances are implemented"); + const msg = { + provide_liquidity: { + assets: [toAsset(assets[0], amounts[0]), toAsset(assets[1], amounts[1])], + slippage_tolerance: slippageTolerance, + min_lp_to_receive: minLpToReceive, + }, + } satisfies PairExecuteMsg; + + return { + msg, + funds: sortNativeFunds([...nativeFunds(assets[0], amounts[0]), ...nativeFunds(assets[1], amounts[1])]), + }; +} + +export function createWithdrawLiquidityMessage(minAssetsToReceive?: Asset[]) { + return { + withdraw_liquidity: { + min_assets_to_receive: minAssetsToReceive && minAssetsToReceive.length > 0 ? minAssetsToReceive : undefined, + }, + } satisfies PairExecuteMsg; +} diff --git a/frontend/src/lib/astroport/poolDiscovery.test.ts b/frontend/src/lib/astroport/poolDiscovery.test.ts new file mode 100644 index 000000000..8bf77748d --- /dev/null +++ b/frontend/src/lib/astroport/poolDiscovery.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from "vitest"; +import type { AssetInfo, PairInfo } from "../generated/Factory.types"; +import { mergeDiscoveredPools, queryAllFactoryPairs } from "./poolDiscovery"; +import { dexRegistry, type RegistryPool } from "../../config/registry"; + +const native = (denom: string): AssetInfo => ({ native_token: { denom } }); +const token = (contract_addr: string): AssetInfo => ({ token: { contract_addr } }); + +function pair(contract_addr: string, asset_infos: AssetInfo[], pair_type: PairInfo["pair_type"] = { xyk: {} }): PairInfo { + return { + asset_infos, + contract_addr, + liquidity_token: `factory/${contract_addr}/astroport/share`, + pair_type, + }; +} + +describe("factory pool discovery", () => { + it("paginates factory pairs with start_after from the previous page", async () => { + const first = pair("juno1first000000000000000000000000000000000000", [native("ujuno"), native("ufoo")]); + const second = pair("juno1second00000000000000000000000000000000000", [native("ujuno"), native("ubar")]); + const third = pair("juno1third000000000000000000000000000000000000", [native("ujuno"), native("ubaz")]); + const query = vi.fn() + .mockResolvedValueOnce({ pairs: [first, second] }) + .mockResolvedValueOnce({ pairs: [third] }); + + await expect(queryAllFactoryPairs(query, 2)).resolves.toEqual([first, second, third]); + expect(query).toHaveBeenNthCalledWith(1, { pairs: { limit: 2 } }); + expect(query).toHaveBeenNthCalledWith(2, { pairs: { start_after: second.asset_infos, limit: 2 } }); + }); + + it("overlays curated metadata and marks unknown factory pools unverified", () => { + const curated: RegistryPool = { + id: "curated-pool", + label: "Curated JUNO / FOO", + pair: "juno1curated0000000000000000000000000000000000", + lpToken: "factory/juno1curated0000000000000000000000000000000000/astroport/share", + type: "xyk", + feeBps: 30, + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6 }, + { kind: "native", id: "ufoo", symbol: "FOO", decimals: 6 }, + ], + explorer: `${dexRegistry.explorerBaseUrl}/wasm/contract/juno1curated0000000000000000000000000000000000`, + enabled: true, + status: "active", + verified: true, + featured: true, + notes: "curated note", + }; + + const pools = mergeDiscoveredPools([ + pair(curated.pair, [native("ujuno"), native("ufoo")]), + pair("juno1unknown0000000000000000000000000000000000", [native("ujuno"), token("juno1token000000000000000000000000000000000000")], { stable: {} }), + ], [curated]); + + expect(pools).toHaveLength(2); + expect(pools[0]).toMatchObject({ id: "curated-pool", label: "Curated JUNO / FOO", verified: true, source: "registry", featured: true }); + expect(pools[1]).toMatchObject({ pair: "juno1unknown0000000000000000000000000000000000", type: "stable", status: "experimental", verified: false, source: "factory", enabled: true }); + expect(pools[1].assets[1]).toMatchObject({ kind: "cw20", id: "juno1token000000000000000000000000000000000000", decimals: 6 }); + }); + + it("does not infer verification from curated registry provenance", () => { + const curated = { ...dexRegistry.pools[0], status: "active" as const, verified: false }; + const [pool] = mergeDiscoveredPools([ + pair(curated.pair, [native("ujuno"), native(curated.assets[1].id)]), + ], [curated]); + + expect(pool).toMatchObject({ source: "registry", status: "active", verified: false }); + }); + + it("keeps curated pools as fallback when factory discovery misses them and skips unsupported custom types", () => { + const curated = { ...dexRegistry.pools[0], verified: false }; + const pools = mergeDiscoveredPools([ + pair("juno1skip000000000000000000000000000000000000", [native("ujuno"), native("uskip")], { custom: "other" }), + pair("juno1cl00000000000000000000000000000000000000", [native("ujuno"), native("ucl")], { custom: "concentrated" }), + ], [curated]); + + expect(pools.some((pool) => pool.pair === curated.pair && pool.verified === false)).toBe(true); + expect(pools.some((pool) => pool.pair === "juno1skip000000000000000000000000000000000000")).toBe(false); + expect(pools.find((pool) => pool.pair === "juno1cl00000000000000000000000000000000000000")?.type).toBe("concentrated"); + }); +}); diff --git a/frontend/src/lib/astroport/poolDiscovery.ts b/frontend/src/lib/astroport/poolDiscovery.ts new file mode 100644 index 000000000..582c0c646 --- /dev/null +++ b/frontend/src/lib/astroport/poolDiscovery.ts @@ -0,0 +1,114 @@ +import { configuredPools, dexRegistry, type RegistryAsset, type RegistryPool } from "../../config/registry"; +import { resolveAssetMetadata } from "../assets/assetMetadata"; +import type { AssetInfo, PairConfig, PairInfo, PairsResponse } from "../generated/Factory.types"; + +export const FACTORY_PAIRS_PAGE_LIMIT = 30; + +export type FactoryPairsQuery = (message: { pairs: { start_after?: AssetInfo[]; limit: number } }) => Promise; + +export type DiscoveredRegistryPool = RegistryPool & { + source: "registry" | "factory"; + verified: boolean; +}; + +function pairTypeName(pairType: PairInfo["pair_type"]): RegistryPool["type"] | undefined { + if ("xyk" in pairType) return "xyk"; + if ("stable" in pairType) return "stable"; + if ("custom" in pairType && /concentrated/i.test(pairType.custom)) return "concentrated"; + return undefined; +} + +function pairTypeKey(pairType: PairInfo["pair_type"] | PairConfig["pair_type"]): string | undefined { + if ("xyk" in pairType) return "xyk"; + if ("stable" in pairType) return "stable"; + if ("custom" in pairType) return `custom:${pairType.custom.toLowerCase()}`; + return undefined; +} + +export function factoryFeeBpsByPairType(pairConfigs: PairConfig[] = []): Map { + const entries = pairConfigs + .map((config) => [pairTypeKey(config.pair_type), config.total_fee_bps] as const) + .filter((entry): entry is readonly [string, number] => Boolean(entry[0])); + return new Map(entries); +} + +function assetId(assetInfo: AssetInfo): string { + if ("native_token" in assetInfo) return assetInfo.native_token.denom; + return assetInfo.token.contract_addr; +} + +function assetKind(assetInfo: AssetInfo): RegistryAsset["kind"] { + if ("token" in assetInfo) return "cw20"; + return assetInfo.native_token.denom.startsWith("ibc/") ? "ibc" : "native"; +} + +function fallbackAsset(assetInfo: AssetInfo): RegistryAsset { + const id = assetId(assetInfo); + const { source: _source, ...metadata } = resolveAssetMetadata(id, { kind: assetKind(assetInfo), id }); + return metadata; +} + +function curatedKey(pool: RegistryPool): string { + return pool.pair; +} + +function discoveredKey(pair: PairInfo): string { + return pair.contract_addr; +} + +export async function queryAllFactoryPairs(queryPairs: FactoryPairsQuery, pageLimit = FACTORY_PAIRS_PAGE_LIMIT): Promise { + const pairs: PairInfo[] = []; + let startAfter: AssetInfo[] | undefined; + + for (;;) { + const response = await queryPairs({ pairs: { ...(startAfter ? { start_after: startAfter } : {}), limit: pageLimit } }); + pairs.push(...response.pairs); + if (response.pairs.length < pageLimit) return pairs; + startAfter = response.pairs.at(-1)?.asset_infos; + if (!startAfter) return pairs; + } +} + +export function mergeDiscoveredPools( + discoveredPairs: PairInfo[], + curatedPools: RegistryPool[] = configuredPools, + feeBpsByPairType: Map = new Map(), +): DiscoveredRegistryPool[] { + const curatedByPair = new Map(curatedPools.map((pool) => [curatedKey(pool), pool])); + const merged = new Map(); + + for (const pair of discoveredPairs) { + const type = pairTypeName(pair.pair_type); + if (!type || pair.asset_infos.length !== 2) continue; + + const curated = curatedByPair.get(discoveredKey(pair)); + const fallbackAssets = [fallbackAsset(pair.asset_infos[0]), fallbackAsset(pair.asset_infos[1])] as [RegistryAsset, RegistryAsset]; + const assets = curated?.assets ?? fallbackAssets; + const label = curated?.label ?? `${assets.map((asset) => asset.symbol).join(" / ")} (${type.toUpperCase()})`; + + merged.set(pair.contract_addr, { + id: curated?.id ?? `factory-${pair.contract_addr}`, + label, + pair: pair.contract_addr, + lpToken: curated?.lpToken ?? pair.liquidity_token, + type: curated?.type ?? type, + feeBps: curated?.feeBps ?? feeBpsByPairType.get(pairTypeKey(pair.pair_type) ?? "") ?? 0, + assets, + explorer: curated?.explorer ?? `${dexRegistry.explorerBaseUrl}/wasm/contract/${pair.contract_addr}`, + enabled: curated?.enabled ?? true, + status: curated?.status ?? "experimental", + featured: curated?.featured, + notes: curated?.notes ?? "Discovered from the factory. Metadata is unverified; review the assets before trading or providing liquidity.", + source: curated ? "registry" : "factory", + verified: curated?.verified === true, + }); + } + + for (const curated of curatedPools) { + if (!merged.has(curated.pair)) { + merged.set(curated.pair, { ...curated, source: "registry", verified: curated.verified === true }); + } + } + + return Array.from(merged.values()).filter((pool) => pool.enabled).sort((a, b) => Number(Boolean(b.featured)) - Number(Boolean(a.featured)) || a.label.localeCompare(b.label)); +} diff --git a/frontend/src/lib/astroport/queries.ts b/frontend/src/lib/astroport/queries.ts new file mode 100644 index 000000000..f36260f41 --- /dev/null +++ b/frontend/src/lib/astroport/queries.ts @@ -0,0 +1,147 @@ +import type { RegistryAsset, RegistryPool } from "../../config/registry"; +import type { ConfigResponse, PairInfo, PairsResponse, QueryMsg as FactoryQueryMsg } from "../generated/Factory.types"; +import type { PoolResponse, QueryMsg as PairQueryMsg, ReverseSimulationResponse, SimulationResponse } from "../generated/Pair.types"; +import type { QueryMsg as RouterQueryMsg, SimulateSwapOperationsResponse, SwapOperation } from "../generated/Router.types"; +import { dexRegistry } from "../../config/registry"; +import { e2ePoolResponse, e2eReverseSwapSimulation, e2eRouterSimulation, e2eSwapSimulation, isE2EMode } from "../../e2e/mocks"; +import { toAsset } from "./assetInfo"; +import { getReadonlyCosmWasmClient } from "../cosmjs/clients"; + +export type PoolAssetResponse = { info: unknown; amount: string }; +export type { PoolResponse, ReverseSimulationResponse, SimulationResponse } from "../generated/Pair.types"; +export type SwapQuoteMode = "exact-in" | "exact-out"; +export type { SimulateSwapOperationsResponse } from "../generated/Router.types"; + +const DEFAULT_REST_TIMEOUT_MS = 8_000; + +function restTimeoutMs() { + const raw = import.meta.env.VITE_DEX_REST_TIMEOUT_MS as string | undefined; + const value = raw ? Number(raw) : DEFAULT_REST_TIMEOUT_MS; + return Number.isFinite(value) && value > 0 ? value : DEFAULT_REST_TIMEOUT_MS; +} + +function encodeSmartQuery(message: unknown): string { + const json = JSON.stringify(message); + const bytes = new TextEncoder().encode(json); + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary); +} + +export async function queryContractSmart(contractAddress: string, message: unknown): Promise { + const encoded = encodeURIComponent(encodeSmartQuery(message)); + const timeoutMs = restTimeoutMs(); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + let response: Response; + try { + response = await fetch(`${dexRegistry.restEndpoint}/cosmwasm/wasm/v1/contract/${contractAddress}/smart/${encoded}`, { signal: controller.signal }); + } catch (error) { + return queryContractSmartRpc(contractAddress, message, error instanceof DOMException && error.name === "AbortError" ? `REST timed out after ${timeoutMs}ms` : "REST unavailable"); + } finally { + clearTimeout(timeout); + } + if (!response.ok) return queryContractSmartRpc(contractAddress, message, `REST returned ${response.status}`); + const payload = await response.json() as { data: T }; + return payload.data; +} + +async function queryContractSmartRpc(contractAddress: string, message: unknown, restFailure: string): Promise { + try { + const client = await getReadonlyCosmWasmClient(); + return await client.queryContractSmart(contractAddress, message) as T; + } catch { + throw new Error(`Smart query unavailable (${restFailure}; RPC fallback failed)`); + } +} + +export async function queryPairPool(pairAddress: string): Promise { + if (isE2EMode()) { + const pool = dexRegistry.pools.find((candidate) => candidate.pair === pairAddress) ?? dexRegistry.pools[0]; + return e2ePoolResponse(pool); + } + return queryContractSmart(pairAddress, { pool: {} } satisfies PairQueryMsg); +} + +export async function queryFactoryPairs(message: Extract): Promise { + if (isE2EMode()) return { pairs: [] } as PairsResponse; + return queryContractSmart(dexRegistry.factory, message); +} + +export async function queryFactoryConfig(): Promise { + if (isE2EMode()) { + return { + owner: "juno1e2eowner000000000000000000000000000000000000", + token_code_id: 1, + pair_configs: [{ code_id: 1, pair_type: { xyk: {} }, total_fee_bps: 30, maker_fee_bps: 10, is_disabled: false, is_generator_disabled: false }], + } as ConfigResponse; + } + return queryContractSmart(dexRegistry.factory, { config: {} } satisfies FactoryQueryMsg); +} + +export async function queryFactoryPair(assetInfos: Extract["pair"]["asset_infos"]): Promise { + if (isE2EMode()) throw new Error("Pair was not found"); + return queryContractSmart(dexRegistry.factory, { pair: { asset_infos: assetInfos } } satisfies FactoryQueryMsg); +} + +export async function querySwapSimulation( + pairAddress: string, + offerAsset: RegistryAsset, + askAsset: RegistryAsset, + amount: string, +): Promise { + if (isE2EMode()) return e2eSwapSimulation(amount); + return queryContractSmart(pairAddress, { + simulation: { + offer_asset: toAsset(offerAsset, amount), + ask_asset_info: askAsset.kind === "cw20" + ? { token: { contract_addr: askAsset.id } } + : { native_token: { denom: askAsset.id } }, + }, + } satisfies PairQueryMsg); +} + +export async function queryReverseSwapSimulation( + pairAddress: string, + offerAsset: RegistryAsset, + askAsset: RegistryAsset, + askAmount: string, +): Promise { + if (isE2EMode()) return e2eReverseSwapSimulation(askAmount); + return queryContractSmart(pairAddress, { + reverse_simulation: { + ask_asset: toAsset(askAsset, askAmount), + offer_asset_info: offerAsset.kind === "cw20" + ? { token: { contract_addr: offerAsset.id } } + : { native_token: { denom: offerAsset.id } }, + }, + } satisfies PairQueryMsg); +} + +export async function queryRouterSimulation(operations: SwapOperation[], offerAmount: string): Promise { + if (isE2EMode()) return e2eRouterSimulation(offerAmount); + if (!dexRegistry.router) throw new Error("Router contract is not configured"); + return queryContractSmart(dexRegistry.router, { + simulate_swap_operations: { + offer_amount: offerAmount, + operations, + }, + } satisfies RouterQueryMsg); +} + +export async function queryRouterReverseSimulation(operations: SwapOperation[], askAmount: string): Promise { + if (isE2EMode()) return e2eRouterSimulation(askAmount); + if (!dexRegistry.router) throw new Error("Router contract is not configured"); + return queryContractSmart(dexRegistry.router, { + reverse_simulate_swap_operations: { + ask_amount: askAmount, + operations, + }, + } satisfies RouterQueryMsg); +} + +export function findOppositeAsset(pool: RegistryPool, offerId: string): RegistryAsset { + const asset = pool.assets.find((candidate) => candidate.id !== offerId); + if (!asset) throw new Error("pool must contain two distinct assets"); + return asset; +} diff --git a/frontend/src/lib/astroport/routes.test.ts b/frontend/src/lib/astroport/routes.test.ts new file mode 100644 index 000000000..6dea5b427 --- /dev/null +++ b/frontend/src/lib/astroport/routes.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryAsset, RegistryPool } from "../../config/registry"; +import { fromBase64, fromUtf8 } from "@cosmjs/encoding"; +import { createCw20RouterSwapSendMessage, createRouterSwapMessage, findSwapRoutes, routeSymbols } from "./routes"; + +const juno: RegistryAsset = { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6 }; +const usdc: RegistryAsset = { kind: "ibc", id: "ibc/usdc", symbol: "USDC", decimals: 6 }; +const atom: RegistryAsset = { kind: "ibc", id: "ibc/atom", symbol: "ATOM", decimals: 6 }; +const whale: RegistryAsset = { kind: "cw20", id: "juno1whale000000000000000000000000000000000000", symbol: "WHALE", decimals: 6 }; + +function pool(id: string, assets: [RegistryAsset, RegistryAsset]): RegistryPool { + return { + id, + label: `${assets[0].symbol} / ${assets[1].symbol}`, + pair: `juno1${id.padEnd(38, "x")}`, + lpToken: `factory/juno1${id}/lp`, + type: "xyk", + feeBps: 30, + assets, + explorer: `https://ping.pub/juno/address/juno1${id}`, + enabled: true, + status: "active", + }; +} + +describe("swap route graph", () => { + it("discovers direct and multi-hop astro_swap routes up to the hop limit", () => { + const routes = findSwapRoutes([ + pool("junousdc", [juno, usdc]), + pool("usdcatom", [usdc, atom]), + pool("atomwhale", [atom, whale]), + ], juno, atom, 3); + + expect(routes.map(routeSymbols)).toEqual(["JUNO → USDC → ATOM"]); + expect(routes[0].operations).toEqual([ + { astro_swap: { offer_asset_info: { native_token: { denom: "ujuno" } }, ask_asset_info: { native_token: { denom: "ibc/usdc" } } } }, + { astro_swap: { offer_asset_info: { native_token: { denom: "ibc/usdc" } }, ask_asset_info: { native_token: { denom: "ibc/atom" } } } }, + ]); + }); + + it("prefers shorter candidates first and excludes routes beyond the hop limit", () => { + const routes = findSwapRoutes([ + pool("junousdc", [juno, usdc]), + pool("usdcatom", [usdc, atom]), + pool("junowhale", [juno, whale]), + pool("whaleatom", [whale, atom]), + pool("junoatom", [juno, atom]), + ], juno, atom, 1); + + expect(routes.map(routeSymbols)).toEqual(["JUNO → ATOM"]); + }); + + it("excludes experimental, deprecated, blocked, and disabled pools", () => { + const active = pool("active", [juno, atom]); + const unavailable = [ + { ...pool("experimental", [juno, atom]), status: "experimental" as const }, + { ...pool("deprecated", [juno, atom]), status: "deprecated" as const }, + { ...pool("blocked", [juno, atom]), status: "blocked" as const }, + { ...pool("disabled", [juno, atom]), enabled: false }, + ]; + + expect(findSwapRoutes(unavailable, juno, atom)).toEqual([]); + expect(findSwapRoutes([...unavailable, active], juno, atom).map((route) => route.hops[0].pool.id)).toEqual(["active"]); + }); + + it("never constructs a route through an explicitly blocked asset", () => { + const blockedUsdc = { ...usdc, blocked: true }; + expect(findSwapRoutes([pool("blockedasset", [juno, blockedUsdc])], juno, blockedUsdc)).toEqual([]); + expect(findSwapRoutes([ + pool("blockedhop", [juno, blockedUsdc]), + pool("blockedatom", [blockedUsdc, atom]), + ], juno, atom)).toEqual([]); + }); +}); + +describe("createRouterSwapMessage", () => { + it("builds execute_swap_operations with astro_swap operations, min receive, max spread, and native funds", () => { + const [route] = findSwapRoutes([pool("junousdc", [juno, usdc]), pool("usdcatom", [usdc, atom])], juno, atom, 2); + const payload = createRouterSwapMessage(route, juno, "1000000", "0.005", "990000"); + + expect(payload).toEqual({ + msg: { + execute_swap_operations: { + operations: route.operations, + minimum_receive: "990000", + max_spread: "0.005", + to: undefined, + }, + }, + funds: [{ denom: "ujuno", amount: "1000000" }], + }); + }); + + it("uses an atomic CW20 send hook for multi-hop router swaps", () => { + const [route] = findSwapRoutes([pool("whaleusdc", [whale, usdc]), pool("usdcatom", [usdc, atom])], whale, atom, 2); + const msg = createCw20RouterSwapSendMessage("juno1router", route, "1000000", "0.005", "990000"); + expect(msg.send).toMatchObject({ contract: "juno1router", amount: "1000000" }); + expect(JSON.parse(fromUtf8(fromBase64(msg.send.msg)))).toEqual({ + execute_swap_operations: { operations: route.operations, minimum_receive: "990000", max_spread: "0.005" }, + }); + }); +}); diff --git a/frontend/src/lib/astroport/routes.ts b/frontend/src/lib/astroport/routes.ts new file mode 100644 index 000000000..c1fdce4c1 --- /dev/null +++ b/frontend/src/lib/astroport/routes.ts @@ -0,0 +1,111 @@ +import { isPoolTradeable, type RegistryAsset, type RegistryPool } from "../../config/registry"; +import { isAssetBlocked } from "../risk"; +import type { ExecuteMsg as RouterExecuteMsg, SwapOperation } from "../generated/Router.types"; +import { nativeFunds, toAssetInfo } from "./assetInfo"; +import { toBase64, toUtf8 } from "@cosmjs/encoding"; + +export type SwapRouteHop = { + pool: RegistryPool; + offerAsset: RegistryAsset; + askAsset: RegistryAsset; +}; + +export type SwapRoute = { + id: string; + hops: SwapRouteHop[]; + operations: SwapOperation[]; +}; + +export function sameAsset(left: RegistryAsset | undefined, right: RegistryAsset | undefined) { + return Boolean(left && right && left.id === right.id); +} + +export function getPoolNeighbor(pool: RegistryPool, assetId: string): RegistryAsset | undefined { + if (pool.assets[0].id === assetId) return pool.assets[1]; + if (pool.assets[1].id === assetId) return pool.assets[0]; + return undefined; +} + +function routeId(hops: SwapRouteHop[]) { + return hops.map((hop) => `${hop.pool.pair}:${hop.offerAsset.id}->${hop.askAsset.id}`).join("|"); +} + +export function routeToOperations(hops: SwapRouteHop[]): SwapOperation[] { + return hops.map((hop) => ({ + astro_swap: { + offer_asset_info: toAssetInfo(hop.offerAsset), + ask_asset_info: toAssetInfo(hop.askAsset), + }, + })); +} + +export function findSwapRoutes(pools: RegistryPool[], offerAsset: RegistryAsset | undefined, askAsset: RegistryAsset | undefined, maxHops = 3): SwapRoute[] { + if (!offerAsset || !askAsset || isAssetBlocked(offerAsset) || isAssetBlocked(askAsset) || offerAsset.id === askAsset.id || maxHops < 1) return []; + + const routes: SwapRoute[] = []; + const seenRoutes = new Set(); + + function visit(currentAsset: RegistryAsset, targetAsset: RegistryAsset, hops: SwapRouteHop[], visitedAssets: Set, usedPairs: Set) { + if (hops.length >= maxHops) return; + + for (const pool of pools) { + if (!isPoolTradeable(pool) || pool.assets.some(isAssetBlocked) || usedPairs.has(pool.pair)) continue; + const nextAsset = getPoolNeighbor(pool, currentAsset.id); + if (!nextAsset || visitedAssets.has(nextAsset.id)) continue; + + const nextHops = [...hops, { pool, offerAsset: currentAsset, askAsset: nextAsset }]; + if (nextAsset.id === targetAsset.id) { + const id = routeId(nextHops); + if (!seenRoutes.has(id)) { + seenRoutes.add(id); + routes.push({ id, hops: nextHops, operations: routeToOperations(nextHops) }); + } + continue; + } + + visit(nextAsset, targetAsset, nextHops, new Set([...visitedAssets, nextAsset.id]), new Set([...usedPairs, pool.pair])); + } + } + + visit(offerAsset, askAsset, [], new Set([offerAsset.id]), new Set()); + return routes.sort((a, b) => a.hops.length - b.hops.length || a.id.localeCompare(b.id)); +} + +export function routeSymbols(route: SwapRoute): string { + if (route.hops.length === 0) return "—"; + return [route.hops[0].offerAsset.symbol, ...route.hops.map((hop) => hop.askAsset.symbol)].join(" → "); +} + +export function createRouterSwapMessage(route: SwapRoute, offerAsset: RegistryAsset, amount: string, maxSpread: string, minimumReceive?: string, to?: string) { + const msg = { + execute_swap_operations: { + operations: route.operations, + minimum_receive: minimumReceive, + max_spread: maxSpread, + to, + }, + } satisfies RouterExecuteMsg; + + return { + msg, + funds: nativeFunds(offerAsset, amount), + }; +} + +export function createCw20RouterSwapSendMessage(routerContract: string, route: SwapRoute, amount: string, maxSpread: string, minimumReceive?: string, to?: string) { + const hook = { + execute_swap_operations: { + operations: route.operations, + minimum_receive: minimumReceive, + max_spread: maxSpread, + to, + }, + }; + return { + send: { + contract: routerContract, + amount, + msg: toBase64(toUtf8(JSON.stringify(hook))), + }, + }; +} diff --git a/frontend/src/lib/cosmjs/clients.ts b/frontend/src/lib/cosmjs/clients.ts new file mode 100644 index 000000000..326c61847 --- /dev/null +++ b/frontend/src/lib/cosmjs/clients.ts @@ -0,0 +1,89 @@ +import type { OfflineSigner } from "@cosmjs/proto-signing"; +import type { EncodeObject } from "@cosmjs/proto-signing"; +import type { Coin } from "@cosmjs/stargate"; +import type { ExecuteResult } from "@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient.js"; +import type { StargateClient as ReadonlyStargateClient } from "@cosmjs/stargate/build/stargateclient.js"; +import { dexRegistry } from "../../config/registry"; +import type { CosmWasmClient as ReadonlyCosmWasmClient } from "@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js"; + +type SigningCosmWasmClientModule = typeof import("@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient.js"); +type StargateClientModule = typeof import("@cosmjs/stargate/build/stargateclient.js"); +type StargateFeeModule = typeof import("@cosmjs/stargate/build/fee.js"); +type CosmWasmClientModule = typeof import("@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js"); + +export type ExecuteClient = { + execute: ( + senderAddress: string, + contractAddress: string, + msg: Record, + fee: "auto" | number, + memo?: string, + funds?: Coin[], + ) => Promise; + simulate?: (signerAddress: string, messages: readonly EncodeObject[], memo: string | undefined) => Promise; +}; + +export type SigningClientGetter = () => Promise; +export type SigningClientSource = OfflineSigner | SigningClientGetter | undefined; + +let readonlyStargateClientPromise: Promise | undefined; +let readonlyCosmWasmClientPromise: Promise | undefined; + +function cjsExport(module: unknown, key: string): T | undefined { + if (!module || typeof module !== "object") return undefined; + const namespace = module as Record; + const defaultExport = namespace.default && typeof namespace.default === "object" + ? namespace.default as Record + : undefined; + return (namespace[key] ?? defaultExport?.[key]) as T | undefined; +} + +async function loadReadonlyStargateClient() { + const module = await import("@cosmjs/stargate/build/stargateclient.js"); + return cjsExport(module, "StargateClient"); +} + +async function loadSigningDependencies() { + const [signingModule, feeModule] = await Promise.all([ + import("@cosmjs/cosmwasm-stargate/build/signingcosmwasmclient.js"), + import("@cosmjs/stargate/build/fee.js"), + ]); + return { + SigningCosmWasmClient: cjsExport(signingModule, "SigningCosmWasmClient"), + GasPrice: cjsExport(feeModule, "GasPrice"), + }; +} + +export async function getReadonlyStargateClient() { + const StargateClient = await loadReadonlyStargateClient(); + if (!StargateClient?.connect) { + throw new Error("CosmJS readonly client failed to initialize"); + } + readonlyStargateClientPromise ??= StargateClient.connect(dexRegistry.rpcEndpoint); + return readonlyStargateClientPromise; +} + +export async function getReadonlyCosmWasmClient() { + const module = await import("@cosmjs/cosmwasm-stargate/build/cosmwasmclient.js"); + const CosmWasmClient = cjsExport(module, "CosmWasmClient"); + if (!CosmWasmClient?.connect) throw new Error("CosmJS query client failed to initialize"); + readonlyCosmWasmClientPromise ??= CosmWasmClient.connect(dexRegistry.rpcEndpoint); + return readonlyCosmWasmClientPromise; +} + +export async function getSigningClient(signer: OfflineSigner) { + const { SigningCosmWasmClient, GasPrice } = await loadSigningDependencies(); + if (!SigningCosmWasmClient?.connectWithSigner || !GasPrice?.fromString) { + throw new Error("CosmJS signing client failed to initialize"); + } + + return SigningCosmWasmClient.connectWithSigner(dexRegistry.rpcEndpoint, signer, { + gasPrice: GasPrice.fromString("0.075ujuno"), + }); +} + +export async function resolveSigningClient(source: SigningClientSource): Promise { + if (!source) return undefined; + if (typeof source === "function") return source(); + return getSigningClient(source); +} diff --git a/frontend/src/lib/cosmjs/fees.test.ts b/frontend/src/lib/cosmjs/fees.test.ts new file mode 100644 index 000000000..17db9e53a --- /dev/null +++ b/frontend/src/lib/cosmjs/fees.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it, vi } from "vitest"; +import { fromUtf8 } from "@cosmjs/encoding"; +import { estimateExecuteNetworkFee, executeInstructionToEncodeObject } from "./fees"; + +describe("network fee estimation", () => { + it("simulates the exact execute message and applies the configured gas adjustment and JUNO gas price", async () => { + const simulate = vi.fn().mockResolvedValue(100_000); + const client = { execute: vi.fn(), simulate }; + const instruction = { contractAddress: "juno1pair", msg: { swap: { max_spread: "0.005" } }, funds: [{ denom: "ujuno", amount: "1000000" }] }; + const estimate = await estimateExecuteNetworkFee(async () => client as never, "juno1sender", [instruction]); + + expect(estimate).toEqual({ amountBase: "9750", amountJuno: "0.00975", gasUsed: 100_000, gasLimit: 130_000, gasPrice: 0.075 }); + const [, messages] = simulate.mock.calls[0]; + expect(messages[0].typeUrl).toBe("/cosmwasm.wasm.v1.MsgExecuteContract"); + expect(fromUtf8(messages[0].value.msg)).toBe(JSON.stringify(instruction.msg)); + expect(messages[0].value.funds).toEqual(instruction.funds); + }); + + it("returns unavailable when the wallet client cannot simulate", async () => { + expect(await estimateExecuteNetworkFee(async () => ({ execute: vi.fn() }) as never, "juno1sender", [{ contractAddress: "juno1pair", msg: {} }])).toBeUndefined(); + }); + + it("builds deterministic execute encode objects", () => { + expect(executeInstructionToEncodeObject("juno1sender", { contractAddress: "juno1pair", msg: { claim: {} } })).toMatchObject({ + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: { sender: "juno1sender", contract: "juno1pair", funds: [] }, + }); + }); +}); diff --git a/frontend/src/lib/cosmjs/fees.ts b/frontend/src/lib/cosmjs/fees.ts new file mode 100644 index 000000000..db8268b14 --- /dev/null +++ b/frontend/src/lib/cosmjs/fees.ts @@ -0,0 +1,57 @@ +import type { Coin } from "@cosmjs/stargate"; +import type { MsgExecuteContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; +import { toUtf8 } from "@cosmjs/encoding"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import { JUNO_CHAIN_INFO } from "../../config/chains"; +import { formatAmount } from "../format/amounts"; +import { resolveSigningClient, type SigningClientSource } from "./clients"; + +export type ExecuteInstruction = { + contractAddress: string; + msg: Record; + funds?: readonly Coin[]; +}; + +export type NetworkFeeEstimate = { + amountBase: string; + amountJuno: string; + gasUsed: number; + gasLimit: number; + gasPrice: number; +}; + +const GAS_ADJUSTMENT = 1.3; +const FEE_CURRENCY = JUNO_CHAIN_INFO.feeCurrencies[0]; + +export function executeInstructionToEncodeObject(sender: string, instruction: ExecuteInstruction): MsgExecuteContractEncodeObject { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender, + contract: instruction.contractAddress, + msg: toUtf8(JSON.stringify(instruction.msg)), + funds: [...(instruction.funds ?? [])], + }), + }; +} + +export async function estimateExecuteNetworkFee( + signerOrClient: SigningClientSource, + sender: string | undefined, + instructions: readonly ExecuteInstruction[], +): Promise { + if (!sender || instructions.length === 0) return undefined; + const client = await resolveSigningClient(signerOrClient); + if (!client?.simulate) return undefined; + const gasUsed = await client.simulate(sender, instructions.map((instruction) => executeInstructionToEncodeObject(sender, instruction)), undefined); + if (!Number.isFinite(gasUsed) || gasUsed <= 0) return undefined; + const gasLimit = Math.ceil(gasUsed * GAS_ADJUSTMENT); + const amountBase = Math.ceil(gasLimit * FEE_CURRENCY.gasPriceStep.average).toString(); + return { + amountBase, + amountJuno: formatAmount(amountBase, FEE_CURRENCY.coinDecimals, FEE_CURRENCY.coinDecimals), + gasUsed, + gasLimit, + gasPrice: FEE_CURRENCY.gasPriceStep.average, + }; +} diff --git a/frontend/src/lib/createPool.test.ts b/frontend/src/lib/createPool.test.ts new file mode 100644 index 000000000..97d2cb78c --- /dev/null +++ b/frontend/src/lib/createPool.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryAsset, RegistryPool } from "../config/registry"; +import type { PairConfig } from "./generated/Factory.types"; +import { createPairMessage, createPoolOptions, extractCreatedPairAddress, makeCustomAsset, validateCreatePool } from "./createPool"; + +const juno: RegistryAsset = { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, verified: true }; +const atom: RegistryAsset = { kind: "ibc", id: "ibc/atom", symbol: "ATOM", decimals: 6, verified: true }; +const unknown: RegistryAsset = { kind: "native", id: "factory/juno1creator/unknown", symbol: "UNKNOWN", decimals: 6, verified: false }; +const cw20: RegistryAsset = { kind: "cw20", id: "juno1cw20contract00000000000000000000000000000", symbol: "CW", decimals: 6, verified: true }; + +const configs: PairConfig[] = [ + { code_id: 1, pair_type: { xyk: {} }, total_fee_bps: 30, maker_fee_bps: 10, permissioned: false }, + { code_id: 2, pair_type: { stable: {} }, total_fee_bps: 5, maker_fee_bps: 2, is_disabled: true }, + { code_id: 3, pair_type: { custom: "concentrated" }, total_fee_bps: 20, maker_fee_bps: 10, permissioned: true }, +]; + +describe("create pool helpers", () => { + it("builds factory create_pair messages from generated Factory types", () => { + expect(createPairMessage([juno, atom], { xyk: {} })).toEqual({ + create_pair: { + pair_type: { xyk: {} }, + asset_infos: [{ native_token: { denom: "ujuno" } }, { native_token: { denom: "ibc/atom" } }], + init_params: undefined, + }, + }); + const cw20Message = createPairMessage([juno, cw20], { xyk: {} }); + if (!("create_pair" in cw20Message)) throw new Error("expected create_pair message"); + expect(cw20Message.create_pair.asset_infos).toEqual([ + { native_token: { denom: "ujuno" } }, + { token: { contract_addr: cw20.id } }, + ]); + }); + + it("maps live factory configs to disabled and permissionless options", () => { + const options = createPoolOptions(configs); + expect(options.find((option) => option.id === "xyk")).toMatchObject({ feeBps: 30, disabled: false }); + expect(options.find((option) => option.id === "stable")).toMatchObject({ disabled: true, unsupportedReason: "This pool type is currently disabled." }); + expect(options.find((option) => option.id === "concentrated")).toMatchObject({ disabled: true, permissioned: true }); + }); + + it("blocks duplicates and unacknowledged unverified assets", () => { + const option = createPoolOptions(configs)[0]; + const existing: RegistryPool = { + id: "existing", + label: "JUNO / ATOM", + pair: "juno1existing00000000000000000000000000000000", + lpToken: "factory/juno1existing/share", + type: "xyk", + feeBps: 30, + assets: [juno, atom], + explorer: "https://example.com", + enabled: true, + status: "active", + }; + + expect(validateCreatePool({ assets: [juno, atom], option, existingPair: existing, riskAcknowledged: true }).error).toBe("A pool already exists for these assets"); + expect(validateCreatePool({ assets: [juno, unknown], option, riskAcknowledged: false })).toMatchObject({ isValid: false, error: "Acknowledge unverified asset risk", requiresAcknowledgement: true }); + expect(validateCreatePool({ assets: [juno, unknown], option, riskAcknowledged: true }).isValid).toBe(true); + expect(validateCreatePool({ assets: [juno, { ...unknown, blocked: true }], option, riskAcknowledged: true })).toMatchObject({ + isValid: false, + error: "Blocked assets cannot be used to create pools", + requiresAcknowledgement: false, + }); + }); + + it("creates fallback custom assets and extracts created pair addresses from tx events", () => { + expect(makeCustomAsset({ kind: "cw20", id: "juno1cw20contract00000000000000000000000000000", symbol: "CW", decimals: 6 })).toMatchObject({ kind: "cw20", symbol: "CW", verified: false }); + expect(extractCreatedPairAddress({ events: [{ type: "wasm", attributes: [{ key: "pair_contract_addr", value: "juno1newpair000000000000000000000000000000000" }] }] })).toBe("juno1newpair000000000000000000000000000000000"); + }); +}); diff --git a/frontend/src/lib/createPool.ts b/frontend/src/lib/createPool.ts new file mode 100644 index 000000000..9650a6756 --- /dev/null +++ b/frontend/src/lib/createPool.ts @@ -0,0 +1,186 @@ +import { dexRegistry, type RegistryAsset, type RegistryPool } from "../config/registry"; +import { getChainRegistryAssets, resolveAssetMetadata } from "./assets/assetMetadata"; +import { toAssetInfo } from "./astroport/assetInfo"; +import type { ExecuteMsg as FactoryExecuteMsg, PairConfig, PairInfo, PairType } from "./generated/Factory.types"; +import { assessAssetRisk, type RiskAssessment } from "./risk"; + +export type CreatePoolType = "xyk" | "stable" | "concentrated"; + +export type CreatePoolConfigOption = { + id: CreatePoolType; + label: string; + pairType: PairType; + feeBps?: number; + disabled?: boolean; + permissioned?: boolean; + unsupportedReason?: string; +}; + +export type CreatePoolValidation = { + isValid: boolean; + error?: string; + warnings: string[]; + requiresAcknowledgement: boolean; + risk: RiskAssessment; +}; + +const typeLabels: Record = { + xyk: "XYK constant product", + stable: "Stable swap", + concentrated: "PCL / concentrated liquidity", +}; + +export function pairTypeKey(pairType: PairType): CreatePoolType | undefined { + if ("xyk" in pairType) return "xyk"; + if ("stable" in pairType) return "stable"; + if ("custom" in pairType && /concentrated|pcl/i.test(pairType.custom)) return "concentrated"; + return undefined; +} + +function defaultPairType(id: CreatePoolType): PairType { + if (id === "xyk") return { xyk: {} }; + if (id === "stable") return { stable: {} }; + return { custom: "concentrated" }; +} + +export function createPoolOptions(pairConfigs: PairConfig[] | undefined): CreatePoolConfigOption[] { + const byType = new Map(); + for (const config of pairConfigs ?? []) { + const key = pairTypeKey(config.pair_type); + if (key && !byType.has(key)) byType.set(key, config); + } + + return (["xyk", "stable", "concentrated"] as const).map((id) => { + const config = byType.get(id); + const unavailable = pairConfigs && !config; + return { + id, + label: typeLabels[id], + pairType: config?.pair_type ?? defaultPairType(id), + feeBps: config?.total_fee_bps, + disabled: unavailable || config?.is_disabled || config?.permissioned, + permissioned: config?.permissioned, + unsupportedReason: unavailable + ? "This pool type is not available on the connected network." + : config?.is_disabled + ? "This pool type is currently disabled." + : config?.permissioned + ? "This pool type requires operator permission and cannot be created from the app." + : undefined, + }; + }); +} + +export function buildCreatePoolAssets(pools: RegistryPool[] = []): Array { + const byId = new Map(); + for (const asset of getChainRegistryAssets()) { + const { source: _source, ...metadata } = asset; + byId.set(metadata.id, { ...metadata, verified: true, poolCount: 0 }); + } + for (const pool of pools) { + for (const asset of pool.assets) { + const existing = byId.get(asset.id); + byId.set(asset.id, { + ...existing, + ...asset, + logoURI: existing?.logoURI ?? asset.logoURI, + verified: existing?.verified === true || asset.verified === true, + poolCount: (existing?.poolCount ?? 0) + 1, + }); + } + } + return Array.from(byId.values()).sort((a, b) => Number(Boolean(b.verified)) - Number(Boolean(a.verified)) || a.symbol.localeCompare(b.symbol)); +} + +export function makeCustomAsset(input: { kind: RegistryAsset["kind"]; id: string; symbol?: string; decimals?: number }): RegistryAsset { + const id = input.id.trim(); + const metadata = resolveAssetMetadata(id, { kind: input.kind, id }); + const { source: _source, ...asset } = metadata; + return { + ...asset, + kind: input.kind, + id, + symbol: input.symbol?.trim() || asset.symbol, + decimals: Number.isInteger(input.decimals) && input.decimals! >= 0 ? input.decimals! : asset.decimals, + verified: false, + }; +} + +export function createPairMessage(assets: [RegistryAsset, RegistryAsset], pairType: PairType, initParams?: string | null): FactoryExecuteMsg { + return { + create_pair: { + pair_type: pairType, + asset_infos: [toAssetInfo(assets[0]), toAssetInfo(assets[1])], + init_params: initParams || undefined, + }, + } satisfies FactoryExecuteMsg; +} + +function assetKey(asset: RegistryAsset): string { + return `${asset.kind}:${asset.id}`; +} + +export function poolMatchesAssets(pool: RegistryPool, assets: [RegistryAsset, RegistryAsset]) { + const poolIds = pool.assets.map((asset) => asset.id).sort().join("|"); + const selectedIds = assets.map((asset) => asset.id).sort().join("|"); + return poolIds === selectedIds; +} + +export function validateCreatePool(input: { + assets: [RegistryAsset | undefined, RegistryAsset | undefined]; + option: CreatePoolConfigOption | undefined; + existingPair?: PairInfo | RegistryPool | null; + riskAcknowledged: boolean; +}): CreatePoolValidation { + const warnings: string[] = [ + "Pool creation is permissionless and irreversible once accepted by the factory.", + "Create only assets whose denoms or CW20 contract addresses you have independently verified.", + ]; + const [assetA, assetB] = input.assets; + if (!assetA || !assetB) return { isValid: false, error: "Choose two assets", warnings, requiresAcknowledgement: false, risk: { verified: false, badges: [], requiresAcknowledgement: false } }; + + const assetARisk = assessAssetRisk(assetA); + const assetBRisk = assessAssetRisk(assetB); + const riskBadges = [...assetARisk.badges, ...assetBRisk.badges]; + const risk: RiskAssessment = { + verified: Boolean(assetA.verified && assetB.verified), + badges: riskBadges.filter((badge, index, all) => all.findIndex((candidate) => candidate.id === badge.id) === index), + requiresAcknowledgement: riskBadges.some((badge) => badge.requiresAcknowledgement), + blocked: Boolean(assetARisk.blocked || assetBRisk.blocked), + }; + + if (risk.blocked) return { isValid: false, error: "Blocked assets cannot be used to create pools", warnings, requiresAcknowledgement: false, risk }; + if (assetKey(assetA) === assetKey(assetB)) return { isValid: false, error: "Choose two different assets", warnings, requiresAcknowledgement: risk.requiresAcknowledgement, risk }; + if (!input.option) return { isValid: false, error: "Choose a pool type", warnings, requiresAcknowledgement: risk.requiresAcknowledgement, risk }; + if (input.option.disabled) return { isValid: false, error: input.option.unsupportedReason ?? "Pool type is not available", warnings, requiresAcknowledgement: risk.requiresAcknowledgement, risk }; + if (input.option.id === "stable" && assetA.decimals !== assetB.decimals) { + warnings.push("Stable pools are intended for closely-pegged assets. Different decimals require extra review."); + } + if (input.option.id === "concentrated") { + warnings.push("PCL pools may require custom parameters on some deployments; this flow uses the factory default init params."); + } + if (input.existingPair) return { isValid: false, error: "A pool already exists for these assets", warnings, requiresAcknowledgement: risk.requiresAcknowledgement, risk }; + if (risk.requiresAcknowledgement && !input.riskAcknowledged) return { isValid: false, error: "Acknowledge unverified asset risk", warnings, requiresAcknowledgement: true, risk }; + return { isValid: true, warnings, requiresAcknowledgement: risk.requiresAcknowledgement, risk }; +} + +export function extractCreatedPairAddress(result: unknown): string | undefined { + const events = (result as { events?: Array<{ type?: string; attributes?: Array<{ key?: string; value?: string }> }> })?.events ?? []; + const candidates: string[] = []; + for (const event of events) { + for (const attr of event.attributes ?? []) { + const key = attr.key ?? ""; + const value = attr.value; + if (!value?.startsWith("juno1") || value === dexRegistry.factory) continue; + if (/pair.*(addr|contract)|contract_addr/.test(key)) candidates.unshift(value); + else if (/_contract_address/.test(key)) candidates.push(value); + } + } + if (candidates[0]) return candidates[0]; + const logs = (result as { logs?: Array<{ events?: Array<{ attributes?: Array<{ key?: string; value?: string }> }> }> })?.logs ?? []; + for (const log of logs) { + const address = extractCreatedPairAddress({ events: log.events }); + if (address) return address; + } + return undefined; +} diff --git a/frontend/src/lib/data-access/indexerFallback.test.ts b/frontend/src/lib/data-access/indexerFallback.test.ts new file mode 100644 index 000000000..28c5f7785 --- /dev/null +++ b/frontend/src/lib/data-access/indexerFallback.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { loadPoolCandles, loadPoolMetrics, loadWalletIndexerData, resetIndexerCircuitBreakerForTests, type IndexerRuntimeConfig } from "./indexerFallback"; + +const pool = { id: "juno-usdc", label: "JUNO / USDC", pair: "juno1pool", lpToken: "factory/lp", type: "xyk", feeBps: 30, assets: [], enabled: true, verified: true, source: "registry" } as unknown as RegistryPool; + +function config(overrides: Partial = {}): IndexerRuntimeConfig { + return { baseUrl: "https://indexer.example", disabled: false, timeoutMs: 50, retry: 0, staleAfterMs: 60_000, circuitBreakerMs: 1_000, ...overrides }; +} + +function json(data: unknown, status = 200) { + return new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" } }); +} + +describe("indexer fallback data access", () => { + beforeEach(() => { + resetIndexerCircuitBreakerForTests(); + vi.restoreAllMocks(); + }); + + it("prefers successful indexer pool metrics and preserves source labels", async () => { + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + return json({ data: [{ pair: pool.pair, tvlUsd: 1234, volume24hUsd: 55, totalApr: 7, incentivized: true, updatedAt: new Date().toISOString(), dataSource: "indexer", isMock: false }], pagination: { limit: 50, nextCursor: null } }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolMetrics([pool], config()); + + expect(result.state.source).toBe("indexer"); + expect(result.data[pool.pair]).toMatchObject({ tvlUsd: 1234, volume24hUsd: 55, totalApr: 7, source: "indexer" }); + }); + + it("falls back gracefully when indexer health fails", async () => { + vi.stubGlobal("fetch", vi.fn(async () => json({ status: "down" }, 503))); + + const result = await loadPoolMetrics([pool], config()); + + expect(result.data).toEqual({}); + expect(result.state).toMatchObject({ source: "fallback", isFallback: true }); + expect(result.state.error?.code).toBe("http"); + }); + + it("retries transient indexer failures before falling back", async () => { + let healthAttempts = 0; + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) { + healthAttempts += 1; + if (healthAttempts === 1) return json({ status: "down" }, 503); + return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + } + return json({ data: [{ pair: pool.pair, tvlUsd: 99, updatedAt: new Date().toISOString(), dataSource: "indexer", isMock: false }], pagination: { limit: 50, nextCursor: null } }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolMetrics([pool], config({ retry: 1 })); + + expect(result.state.source).toBe("indexer"); + expect(result.data[pool.pair]?.tvlUsd).toBe(99); + expect(healthAttempts).toBe(2); + }); + + it("falls back gracefully on indexer timeout", async () => { + vi.stubGlobal("fetch", vi.fn((_url: string, init?: RequestInit) => new Promise((_resolve, reject) => { + init?.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError"))); + }))); + + const result = await loadPoolMetrics([pool], config({ timeoutMs: 1 })); + + expect(result.data).toEqual({}); + expect(result.state.error?.code).toBe("timeout"); + }); + + it("treats empty production responses as unavailable instead of fake zeros", async () => { + const fetcher = vi.fn(async (url: string) => url.endsWith("/health") + ? json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }) + : json({ data: [], pagination: { limit: 50, nextCursor: null } })); + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolMetrics([pool], config()); + + expect(result.data).toEqual({}); + expect(result.state.error?.code).toBe("empty"); + expect(result.state.source).toBe("fallback"); + }); + + it("labels mock and stale indexer metrics", async () => { + const staleDate = new Date(Date.now() - 10_000).toISOString(); + const fetcher = vi.fn(async (url: string) => url.endsWith("/health") + ? json({ status: "ok", service: "dex-indexer", dataSource: "mock", isMock: true }) + : json({ data: [{ pair: pool.pair, tvlUsd: 1, updatedAt: staleDate, dataSource: "mock", isMock: true }], pagination: { limit: 50, nextCursor: null } })); + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolMetrics([pool], config({ staleAfterMs: 1 })); + + expect(result.state).toMatchObject({ source: "mock", isMock: true, isStale: true }); + expect(result.data[pool.pair]).toMatchObject({ source: "mock", isMock: true, isStale: true }); + }); + + it("loads pool candles and preserves mock/stale source labels", async () => { + vi.spyOn(Date, "now").mockReturnValue(new Date("2026-07-02T12:00:00.000Z").getTime()); + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "mock", isMock: true }); + expect(url).toContain("/pools/juno1pool/candles?interval=1h"); + expect(url).not.toContain("baseAsset="); + expect(url).not.toContain("quoteAsset="); + return json({ + data: [{ poolId: pool.pair, pairAddress: pool.pair, baseAsset: "ujuno", quoteAsset: "ibc%2Fusdc", interval: "1h", bucketStart: "2026-07-02T10:00:00.000Z", open: 1, high: 1.2, low: 0.9, close: 1.1, volume: 10, volumeQuote: 11, tradeCount: 2, dataSource: "mock", isMock: true }], + pagination: { limit: 200, nextCursor: null }, + meta: { dataSource: "mock", isMock: true }, + }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const candlePool = { ...pool, assets: [{ id: "ujuno" }, { id: "ibc/usdc" }] } as RegistryPool; + const result = await loadPoolCandles(candlePool, { interval: "1h", range: "24h" }, config({ staleAfterMs: 1 })); + + expect(result.data).toHaveLength(1); + expect(result.data[0].close).toBe(1.1); + expect(result.state).toMatchObject({ source: "mock", isMock: true, isStale: true }); + }); + + it("normalizes pool candles to chronological order for chart rendering", async () => { + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + return json({ + data: [ + { poolId: pool.pair, pairAddress: pool.pair, baseAsset: "ujuno", quoteAsset: "ibc/usdc", interval: "1h", bucketStart: "2026-07-02T11:00:00.000Z", open: 1.2, high: 1.3, low: 1.1, close: 1.25, volume: 12, volumeQuote: 15, tradeCount: 2, dataSource: "indexer", isMock: false }, + { poolId: pool.pair, pairAddress: pool.pair, baseAsset: "ujuno", quoteAsset: "ibc/usdc", interval: "1h", bucketStart: "2026-07-02T10:00:00.000Z", open: 1, high: 1.2, low: 0.9, close: 1.1, volume: 10, volumeQuote: 11, tradeCount: 1, dataSource: "indexer", isMock: false }, + ], + pagination: { limit: 200, nextCursor: null }, + meta: { dataSource: "indexer", isMock: false }, + }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolCandles(pool, { interval: "1h" }, config()); + + expect(result.data.map((candle) => candle.bucketStart)).toEqual([ + "2026-07-02T10:00:00.000Z", + "2026-07-02T11:00:00.000Z", + ]); + }); + + it("loads latest available candles when the selected candle range is empty", async () => { + vi.spyOn(Date, "now").mockReturnValue(new Date("2026-07-05T12:00:00.000Z").getTime()); + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + if (url.includes("from=")) { + return json({ data: [], pagination: { limit: 200, nextCursor: null }, meta: { dataSource: "indexer", isMock: false } }); + } + return json({ + data: [{ poolId: pool.pair, pairAddress: pool.pair, baseAsset: "ujuno", quoteAsset: "ibc/usdc", interval: "1h", bucketStart: "2026-07-05T00:00:00.000Z", open: 1, high: 1.2, low: 0.9, close: 1.1, volume: 10, volumeQuote: 11, tradeCount: 1, dataSource: "indexer", isMock: false }], + pagination: { limit: 200, nextCursor: null }, + meta: { dataSource: "indexer", isMock: false }, + }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolCandles(pool, { interval: "1h", range: "24h" }, config()); + const candleUrls = vi.mocked(fetcher).mock.calls.map((call) => String(call[0])).filter((url) => url.includes("/candles")); + + expect(result.data).toHaveLength(1); + expect(result.state.rangeFallback).toBe(true); + expect(candleUrls).toHaveLength(2); + expect(candleUrls[0]).toContain("from="); + expect(candleUrls[1]).not.toContain("from="); + }); + + it("passes explicit candle asset filters through when requested", async () => { + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + expect(url).toContain("baseAsset=ujuno"); + expect(url).toContain("quoteAsset=ibc%2Fusdc"); + return json({ + data: [{ poolId: pool.pair, pairAddress: pool.pair, baseAsset: "ujuno", quoteAsset: "ibc/usdc", interval: "1h", bucketStart: "2026-07-02T10:00:00.000Z", open: 1, high: 1, low: 1, close: 1, volume: 10, volumeQuote: 10, tradeCount: 1, dataSource: "indexer", isMock: false }], + pagination: { limit: 200, nextCursor: null }, + meta: { dataSource: "indexer", isMock: false }, + }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolCandles(pool, { interval: "1h", baseAsset: "ujuno", quoteAsset: "ibc/usdc" }, config()); + + expect(result.data).toHaveLength(1); + }); + + it("keeps empty candle responses empty instead of generating fake chart data", async () => { + const fetcher = vi.fn(async (url: string) => url.endsWith("/health") + ? json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }) + : json({ data: [], pagination: { limit: 200, nextCursor: null }, meta: { dataSource: "indexer", isMock: false } })); + vi.stubGlobal("fetch", fetcher); + + const result = await loadPoolCandles(pool, { interval: "1h" }, config()); + + expect(result.data).toEqual([]); + expect(result.state).toMatchObject({ source: "indexer", isFallback: false, isMock: false }); + }); + + it("loads indexed wallet positions and transaction history without falling back", async () => { + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + if (url.includes("/positions")) return json({ data: [], pagination: { limit: 100, nextCursor: null } }); + return json({ data: [{ txHash: "ABC", walletAddress: "juno1wallet", poolId: "juno-usdc", pairAddress: pool.pair, type: "swap", height: 10, timestamp: new Date().toISOString(), offerAsset: { denom: "ujuno", symbol: "JUNO", amount: "1" }, askAsset: { denom: "ibc/usdc", symbol: "USDC", amount: "2" }, amountUsd: 2, feeUsd: 0.01, success: true, dataSource: "indexer", isMock: false }], pagination: { limit: 50, nextCursor: null } }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadWalletIndexerData("juno1wallet", config()); + + expect(result.state).toMatchObject({ source: "indexer", isFallback: false }); + expect(result.data.history).toHaveLength(1); + expect(result.data.history[0]).toMatchObject({ type: "swap", txHash: "ABC", pairAddress: pool.pair }); + }); + + it("keeps empty wallet history empty instead of generating fake activity", async () => { + const fetcher = vi.fn(async (url: string) => url.endsWith("/health") + ? json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }) + : json({ data: [], pagination: { limit: 100, nextCursor: null } })); + vi.stubGlobal("fetch", fetcher); + + const result = await loadWalletIndexerData("juno1wallet", config()); + + expect(result.state).toMatchObject({ source: "indexer", isFallback: false }); + expect(result.data.history).toEqual([]); + expect(result.data.positions).toEqual([]); + }); + + it("falls back to empty wallet history when the indexer wallet endpoint fails", async () => { + const fetcher = vi.fn(async (url: string) => { + if (url.endsWith("/health")) return json({ status: "ok", service: "dex-indexer", dataSource: "indexer", isMock: false }); + if (url.includes("/history")) return json({ error: "down" }, 500); + return json({ data: [], pagination: { limit: 100, nextCursor: null } }); + }) as unknown as typeof fetch; + vi.stubGlobal("fetch", fetcher); + + const result = await loadWalletIndexerData("juno1wallet", config()); + + expect(result.data).toEqual({ positions: [], history: [] }); + expect(result.state).toMatchObject({ source: "fallback", isFallback: true }); + expect(result.state.error?.code).toBe("http"); + }); +}); diff --git a/frontend/src/lib/data-access/indexerFallback.ts b/frontend/src/lib/data-access/indexerFallback.ts new file mode 100644 index 000000000..c1252f6dc --- /dev/null +++ b/frontend/src/lib/data-access/indexerFallback.ts @@ -0,0 +1,341 @@ +import type { RegistryPool } from "../../config/registry"; +import { isE2EMode } from "../../e2e/mocks"; +import { createIndexerClient, getConfiguredIndexerBaseUrl, IndexerRequestError } from "../indexer/client"; +import type { IndexerCandleInterval, IndexerPoolCandle, IndexerPoolMetrics, IndexerPoolPosition, IndexerWalletTransaction } from "../indexer/types"; +import type { PoolMetrics, PoolMetricsByPair } from "../pools/poolList"; + +export type DataSourceKind = "indexer" | "mock" | "fallback" | "disabled"; +export type DataAccessErrorCode = "disabled" | "health" | "timeout" | "http" | "network" | "empty" | "invalid-response"; + +export type DataAccessState = { + source: DataSourceKind; + isFallback: boolean; + isMock: boolean; + isStale: boolean; + updatedAt?: string; + rangeFallback?: boolean; + error?: { + code: DataAccessErrorCode; + message: string; + status?: number; + }; +}; + +export type DataAccessResult = { + data: T; + state: DataAccessState; +}; + +export type PoolCandleRange = "24h" | "7d" | "30d" | "90d"; +export type PoolCandlesOptions = { + interval?: IndexerCandleInterval; + range?: PoolCandleRange; + limit?: number; + baseAsset?: string; + quoteAsset?: string; +}; + +const RANGE_MS: Record = { + "24h": 24 * 60 * 60 * 1000, + "7d": 7 * 24 * 60 * 60 * 1000, + "30d": 30 * 24 * 60 * 60 * 1000, + "90d": 90 * 24 * 60 * 60 * 1000, +}; + +export type IndexerRuntimeConfig = { + baseUrl?: string; + disabled: boolean; + timeoutMs: number; + retry: number; + staleAfterMs: number; + circuitBreakerMs: number; +}; + +let circuitOpenUntil = 0; +let lastFailure: DataAccessState["error"] | undefined; + +function envNumber(name: string, fallback: number) { + const raw = import.meta.env[name] as string | undefined; + if (!raw) return fallback; + const value = Number(raw); + return Number.isFinite(value) && value >= 0 ? value : fallback; +} + +export function getIndexerRuntimeConfig(): IndexerRuntimeConfig { + const disabled = (import.meta.env.VITE_DEX_INDEXER_DISABLED as string | undefined)?.toLowerCase() === "true"; + return { + baseUrl: getConfiguredIndexerBaseUrl(), + disabled, + timeoutMs: envNumber("VITE_DEX_INDEXER_TIMEOUT_MS", 2_500), + retry: envNumber("VITE_DEX_INDEXER_RETRY", 1), + staleAfterMs: envNumber("VITE_DEX_INDEXER_STALE_AFTER_MS", 120_000), + circuitBreakerMs: envNumber("VITE_DEX_INDEXER_CIRCUIT_BREAKER_MS", 60_000), + }; +} + +export function resetIndexerCircuitBreakerForTests() { + circuitOpenUntil = 0; + lastFailure = undefined; +} + +function optionalNumber(value: unknown) { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return undefined; +} + +function normalizeCandles(rows: IndexerPoolCandle[]) { + return rows + .map((row) => normalizeCandle(row)) + .filter((row): row is IndexerPoolCandle => Boolean(row)) + .sort((left, right) => Date.parse(left.bucketStart) - Date.parse(right.bucketStart)); +} + +function toAccessError(error: unknown, fallbackCode: DataAccessErrorCode): DataAccessState["error"] { + if (error instanceof IndexerRequestError) { + return { code: error.code === "disabled" ? "disabled" : error.code, message: error.message, status: error.status }; + } + return { code: fallbackCode, message: error instanceof Error ? error.message : String(error) }; +} + +function fallbackState(error: DataAccessState["error"], source: DataSourceKind = "fallback"): DataAccessState { + return { source, isFallback: source !== "indexer" && source !== "mock", isMock: false, isStale: false, error }; +} + +export function dataSourceLabel(state: DataAccessState | undefined) { + if (!state) return "Live data"; + if (state.source === "mock") return "Preview data"; + if (state.source === "indexer") return "Live data"; + if (state.source === "disabled") return "Live on-chain data"; + return "Live on-chain data"; +} + +function normalizePoolMetric(row: Partial & Record, staleAfterMs: number): [string, PoolMetrics] | undefined { + const pair = (row.pair ?? row.pairAddress ?? row.pair_address ?? row.address) as string | undefined; + if (!pair) return undefined; + const updatedAt = typeof row.updatedAt === "string" ? row.updatedAt : undefined; + const isMock = Boolean(row.isMock || row.dataSource === "mock"); + const isStale = updatedAt ? Date.now() - Date.parse(updatedAt) > staleAfterMs : false; + return [pair, { + tvlUsd: optionalNumber(row.tvlUsd ?? row.tvl_usd), + tvlJuno: optionalNumber(row.tvlJuno ?? row.tvl_juno), + volume24hUsd: optionalNumber(row.volume24hUsd ?? row.volume_24h_usd ?? row.volume24h_usd), + volume24hJuno: optionalNumber(row.volume24hJuno ?? row.volume_24h_juno ?? row.volume24h_juno), + feeApr: optionalNumber(row.feeApr ?? row.fee_apr), + incentivesApr: optionalNumber(row.incentivesApr ?? row.incentives_apr), + totalApr: optionalNumber(row.totalApr ?? row.total_apr), + incentivized: Boolean(row.incentivized), + source: isMock ? "mock" : "indexer", + isMock, + isStale, + updatedAt, + }]; +} +function normalizeCandle(row: Partial & Record): IndexerPoolCandle | undefined { + const bucketStart = (row.bucketStart ?? row.bucket_start) as string | undefined; + const open = optionalNumber(row.open); + const high = optionalNumber(row.high); + const low = optionalNumber(row.low); + const close = optionalNumber(row.close); + if (!bucketStart || open === undefined || high === undefined || low === undefined || close === undefined) return undefined; + return { + poolId: (row.poolId ?? row.pool_id ?? row.pairAddress ?? row.pair_address ?? null) as string | null, + pairAddress: (row.pairAddress ?? row.pair_address ?? row.poolId ?? row.pool_id ?? null) as string | null, + baseAsset: (row.baseAsset ?? row.base_asset ?? null) as string | null, + quoteAsset: (row.quoteAsset ?? row.quote_asset ?? null) as string | null, + interval: (row.interval ?? "1h") as IndexerPoolCandle["interval"], + bucketStart, + open, + high, + low, + close, + volume: optionalNumber(row.volume) ?? 0, + volumeQuote: optionalNumber(row.volumeQuote ?? row.volume_quote ?? row.volume_usd) ?? 0, + tradeCount: optionalNumber(row.tradeCount ?? row.trade_count) ?? 0, + dataSource: (row.dataSource ?? row.data_source ?? "indexer") as IndexerPoolCandle["dataSource"], + isMock: Boolean(row.isMock ?? row.is_mock), + }; +} + +async function withAttempts(attempts: number, fn: () => Promise) { + let lastError: unknown; + for (let attempt = 0; attempt <= attempts; attempt += 1) { + try { + return await fn(); + } catch (error) { + lastError = error; + } + } + throw lastError; +} + +function shouldUseFallback(config: IndexerRuntimeConfig): DataAccessState | undefined { + if (config.disabled || !config.baseUrl) { + return fallbackState({ code: "disabled", message: config.disabled ? "Indexer disabled by configuration" : "Indexer URL is not configured" }, "disabled"); + } + if (Date.now() < circuitOpenUntil) { + return fallbackState(lastFailure ?? { code: "network", message: "Indexer circuit breaker is open" }); + } + return undefined; +} + +function openCircuit(config: IndexerRuntimeConfig, error: DataAccessState["error"]) { + lastFailure = error; + circuitOpenUntil = Date.now() + config.circuitBreakerMs; +} + +export async function loadPoolMetrics(pools: RegistryPool[], config = getIndexerRuntimeConfig()): Promise> { + if (isE2EMode()) { + return { + data: Object.fromEntries(pools.map((pool) => [pool.pair, { tvlUsd: 125000, volume24hUsd: 42000, feeApr: 8.5, incentivesApr: 12.25, totalApr: 20.75, incentivized: true, source: "mock", isMock: true, isStale: false, updatedAt: new Date(0).toISOString() }])), + state: { source: "mock", isFallback: false, isMock: true, isStale: false, updatedAt: new Date(0).toISOString() }, + }; + } + const earlyFallback = shouldUseFallback(config); + if (earlyFallback) return { data: {}, state: earlyFallback }; + try { + const client = createIndexerClient({ baseUrl: config.baseUrl!, timeoutMs: config.timeoutMs }); + const health = await withAttempts(config.retry, () => client.health()); + if (health.status !== "ok") throw new IndexerRequestError(`Indexer health is ${health.status}`, { code: "invalid-response" }); + const payload = await withAttempts(config.retry, () => client.pools({ limit: Math.max(pools.length, 50) })); + const entries = payload.data.map((row) => normalizePoolMetric(row, config.staleAfterMs)).filter((entry): entry is [string, PoolMetrics] => Boolean(entry)); + if (entries.length === 0) { + return { data: {}, state: fallbackState({ code: "empty", message: "Indexer returned no pool metrics; using on-chain reserve fallback" }) }; + } + const first = entries.find(([, metric]) => metric.isMock || metric.isStale)?.[1]; + return { + data: Object.fromEntries(entries), + state: { + source: first?.isMock ? "mock" : "indexer", + isFallback: false, + isMock: Boolean(first?.isMock), + isStale: Boolean(first?.isStale), + updatedAt: first?.updatedAt, + }, + }; + } catch (error) { + const accessError = toAccessError(error, "network"); + openCircuit(config, accessError); + return { data: {}, state: fallbackState(accessError) }; + } +} + +export async function loadPoolCandles(pool: RegistryPool | undefined, options: PoolCandlesOptions = {}, config = getIndexerRuntimeConfig()): Promise> { + if (!pool) return { data: [], state: fallbackState({ code: "disabled", message: "Pool is not selected" }, "disabled") }; + const earlyFallback = shouldUseFallback(config); + if (earlyFallback) return { data: [], state: earlyFallback }; + try { + const client = createIndexerClient({ baseUrl: config.baseUrl!, timeoutMs: config.timeoutMs }); + const interval = options.interval ?? "1h"; + const range = options.range ?? "7d"; + const to = new Date().toISOString(); + const from = new Date(Date.now() - RANGE_MS[range]).toISOString(); + const health = await withAttempts(config.retry, () => client.health()); + if (health.status !== "ok") throw new IndexerRequestError(`Indexer health is ${health.status}`, { code: "invalid-response" }); + let rangeFallback = false; + let payload = await withAttempts(config.retry, () => client.poolCandles(pool.pair, { + interval, + from, + to, + baseAsset: options.baseAsset, + quoteAsset: options.quoteAsset, + limit: options.limit ?? 200, + })); + let candles = normalizeCandles(payload.data); + if (candles.length === 0) { + const latestPayload = await withAttempts(config.retry, () => client.poolCandles(pool.pair, { + interval, + baseAsset: options.baseAsset, + quoteAsset: options.quoteAsset, + limit: options.limit ?? 200, + })); + const latestCandles = normalizeCandles(latestPayload.data); + if (latestCandles.length > 0) { + payload = latestPayload; + candles = latestCandles; + rangeFallback = true; + } + } + const first = candles.find((candle) => candle.isMock) ?? candles[0]; + const updatedAt = candles.at(-1)?.bucketStart; + const isMock = Boolean(payload.meta?.isMock || first?.isMock || payload.meta?.dataSource === "mock"); + const isStale = updatedAt ? Date.now() - Date.parse(updatedAt) > config.staleAfterMs : false; + return { data: candles, state: { source: isMock ? "mock" : "indexer", isFallback: false, isMock, isStale, updatedAt, rangeFallback } }; + } catch (error) { + const accessError = toAccessError(error, "network"); + openCircuit(config, accessError); + return { data: [], state: fallbackState(accessError) }; + } +} + +export async function loadWalletIndexerData(address: string | undefined, config = getIndexerRuntimeConfig()): Promise> { + const empty = { positions: [], history: [] }; + if (!address) return { data: empty, state: fallbackState({ code: "disabled", message: "Wallet is not connected" }, "disabled") }; + if (isE2EMode()) { + return { + data: { + positions: [], + history: [{ + txHash: "E2E_MOCK_TX_SWAP_000", + walletAddress: address, + poolId: "juno-agent-preview-xyk-1", + pairAddress: "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + type: "swap", + height: 123456, + timestamp: new Date(0).toISOString(), + offerAsset: { denom: "ujuno", symbol: "JUNO", amount: "1000000" }, + askAsset: { denom: "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", symbol: "JUNOAGENT-TEST", amount: "1970000" }, + amountUsd: 1.23, + feeUsd: 0.01, + success: true, + dataSource: "mock", + isMock: true, + }], + }, + state: { source: "mock", isFallback: false, isMock: true, isStale: false, updatedAt: new Date(0).toISOString() }, + }; + } + const earlyFallback = shouldUseFallback(config); + if (earlyFallback) return { data: empty, state: earlyFallback }; + try { + const client = createIndexerClient({ baseUrl: config.baseUrl!, timeoutMs: config.timeoutMs }); + await withAttempts(config.retry, () => client.health()); + const [positions, history] = await Promise.all([ + withAttempts(config.retry, () => client.walletPositions(address, { limit: 100 })), + withAttempts(config.retry, () => client.walletHistory(address, { limit: 50 })), + ]); + const firstPosition = positions.data[0]; + const firstHistory = history.data[0]; + const first = firstPosition ?? firstHistory; + const updatedAt = firstPosition?.updatedAt ?? firstHistory?.timestamp; + const isMock = Boolean(first?.isMock || first?.dataSource === "mock"); + const isStale = updatedAt ? Date.now() - Date.parse(updatedAt) > config.staleAfterMs : false; + return { data: { positions: positions.data, history: history.data }, state: { source: isMock ? "mock" : "indexer", isFallback: false, isMock, isStale, updatedAt } }; + } catch (error) { + const accessError = toAccessError(error, "network"); + openCircuit(config, accessError); + return { data: empty, state: fallbackState(accessError) }; + } +} + +export async function loadPoolActivity(pool: RegistryPool | undefined, limit = 10, config = getIndexerRuntimeConfig()): Promise> { + if (!pool) return { data: [], state: fallbackState({ code: "disabled", message: "Pool is not selected" }, "disabled") }; + const earlyFallback = shouldUseFallback(config); + if (earlyFallback) return { data: [], state: earlyFallback }; + try { + const client = createIndexerClient({ baseUrl: config.baseUrl!, timeoutMs: config.timeoutMs }); + await withAttempts(config.retry, () => client.health()); + const history = await withAttempts(config.retry, () => client.poolHistory(pool.pair, { limit })); + const first = history.data[0]; + const isMock = Boolean(first?.isMock || first?.dataSource === "mock"); + const updatedAt = first?.timestamp; + const isStale = updatedAt ? Date.now() - Date.parse(updatedAt) > config.staleAfterMs : false; + return { data: history.data.slice(0, limit), state: { source: isMock ? "mock" : "indexer", isFallback: false, isMock, isStale, updatedAt } }; + } catch (error) { + const accessError = toAccessError(error, "network"); + return { data: [], state: fallbackState(accessError) }; + } +} diff --git a/frontend/src/lib/format/addresses.ts b/frontend/src/lib/format/addresses.ts new file mode 100644 index 000000000..baa0c22e6 --- /dev/null +++ b/frontend/src/lib/format/addresses.ts @@ -0,0 +1,4 @@ +export function truncateAddress(address: string, prefix = 10, suffix = 8): string { + if (address.length <= prefix + suffix + 3) return address; + return `${address.slice(0, prefix)}…${address.slice(-suffix)}`; +} diff --git a/frontend/src/lib/format/amounts.test.ts b/frontend/src/lib/format/amounts.test.ts new file mode 100644 index 000000000..4e94363c9 --- /dev/null +++ b/frontend/src/lib/format/amounts.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { formatAmount, isBaseAmountGreaterThan, parseTokenAmount, toBaseAmount } from "./amounts"; + +describe("amount formatting", () => { + it("parses zero without floating point math", () => { + expect(parseTokenAmount("0", 6)).toMatchObject({ isValid: true, baseAmount: "0" }); + expect(toBaseAmount("0.000000", 6)).toBe("0"); + }); + + it("parses tiny decimal-safe amounts", () => { + expect(toBaseAmount("0.000001", 6)).toBe("1"); + expect(toBaseAmount("0.000000000000000001", 18)).toBe("1"); + }); + + it("parses huge values as strings", () => { + expect(toBaseAmount("12345678901234567890.123456", 6)).toBe("12345678901234567890123456"); + expect(formatAmount("12345678901234567890123456", 6, 6)).toBe("12,345,678,901,234,567,890.123456"); + }); + + it("rejects invalid inputs", () => { + expect(parseTokenAmount("1.2.3", 6).isValid).toBe(false); + expect(parseTokenAmount("abc", 6).isValid).toBe(false); + expect(parseTokenAmount("0.0000001", 6).isValid).toBe(false); + expect(toBaseAmount("abc", 6)).toBe("0"); + }); + + it("compares balances as bigint-safe base amounts", () => { + expect(isBaseAmountGreaterThan("1000001", "1000000")).toBe(true); + expect(isBaseAmountGreaterThan("999999", "1000000")).toBe(false); + }); +}); diff --git a/frontend/src/lib/format/amounts.ts b/frontend/src/lib/format/amounts.ts new file mode 100644 index 000000000..20952e833 --- /dev/null +++ b/frontend/src/lib/format/amounts.ts @@ -0,0 +1,69 @@ +const DECIMAL_INPUT_PATTERN = /^\d*(?:\.\d*)?$/; + +export type ParsedTokenAmount = { + input: string; + decimals: number; + baseAmount: string; + isValid: boolean; + error?: string; +}; + +function normalizeBaseAmount(amount: string | number | bigint): string { + const raw = String(amount).trim(); + if (raw === "") return "0"; + if (!/^\d+$/.test(raw)) return "0"; + const normalized = raw.replace(/^0+(?=\d)/, ""); + return normalized === "" ? "0" : normalized; +} + +export function parseTokenAmount(value: string, decimals: number): ParsedTokenAmount { + const input = value.trim(); + if (!Number.isInteger(decimals) || decimals < 0) { + return { input: value, decimals, baseAmount: "0", isValid: false, error: "Invalid decimal precision" }; + } + if (input === "") return { input: value, decimals, baseAmount: "0", isValid: true }; + if (!DECIMAL_INPUT_PATTERN.test(input) || input === ".") { + return { input: value, decimals, baseAmount: "0", isValid: false, error: "Enter a valid decimal amount" }; + } + + const [wholePart = "", fractionPart = ""] = input.split("."); + if (fractionPart.length > decimals) { + return { + input: value, + decimals, + baseAmount: "0", + isValid: false, + error: `Too many decimal places; max ${decimals}`, + }; + } + + const whole = wholePart.replace(/^0+(?=\d)/, "") || "0"; + const fraction = fractionPart.padEnd(decimals, "0"); + const combined = `${whole}${fraction}`.replace(/^0+(?=\d)/, ""); + return { input: value, decimals, baseAmount: combined === "" ? "0" : combined, isValid: true }; +} + +export function formatBaseAmount(amount: string | number | bigint | undefined, decimals = 6, maxFractionDigits = 6): string { + if (amount === undefined || amount === null || amount === "") return "—"; + if (!Number.isInteger(decimals) || decimals < 0) return String(amount); + const normalized = normalizeBaseAmount(amount); + const padded = normalized.padStart(decimals + 1, "0"); + const whole = decimals === 0 ? padded : padded.slice(0, -decimals); + const fraction = decimals === 0 ? "" : padded.slice(-decimals).replace(/0+$/, ""); + const trimmedFraction = maxFractionDigits >= 0 ? fraction.slice(0, maxFractionDigits) : fraction; + const groupedWhole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return trimmedFraction ? `${groupedWhole}.${trimmedFraction}` : groupedWhole; +} + +export function formatAmount(amount: string | number | bigint | undefined, decimals = 6, maxFractionDigits = 6): string { + return formatBaseAmount(amount, decimals, maxFractionDigits); +} + +export function toBaseAmount(value: string, decimals: number): string { + const parsed = parseTokenAmount(value, decimals); + return parsed.isValid ? parsed.baseAmount : "0"; +} + +export function isBaseAmountGreaterThan(amount: string, compareTo: string): boolean { + return BigInt(normalizeBaseAmount(amount)) > BigInt(normalizeBaseAmount(compareTo)); +} diff --git a/frontend/src/lib/generated/Factory.client.ts b/frontend/src/lib/generated/Factory.client.ts new file mode 100644 index 000000000..125e5bee6 --- /dev/null +++ b/frontend/src/lib/generated/Factory.client.ts @@ -0,0 +1,267 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { ICosmWasmClient, ISigningCosmWasmClient } from "./baseClient"; +import { Coin, StdFee } from "@interchainjs/types"; +import { PairType, InstantiateMsg, PairConfig, TrackerConfig, ExecuteMsg, AssetInfo, Addr, Binary, QueryMsg, ArrayOfPairType, ConfigResponse, FeeInfoResponse, PairInfo, PairsResponse } from "./Factory.types"; +export interface FactoryReadOnlyInterface { + contractAddress: string; + config: () => Promise; + pair: ({ + assetInfos + }: { + assetInfos: AssetInfo[]; + }) => Promise; + pairs: ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: AssetInfo[]; + }) => Promise; + feeInfo: ({ + pairType + }: { + pairType: PairType; + }) => Promise; + blacklistedPairTypes: () => Promise; + trackerConfig: () => Promise; +} +export class FactoryQueryClient implements FactoryReadOnlyInterface { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.config = this.config.bind(this); + this.pair = this.pair.bind(this); + this.pairs = this.pairs.bind(this); + this.feeInfo = this.feeInfo.bind(this); + this.blacklistedPairTypes = this.blacklistedPairTypes.bind(this); + this.trackerConfig = this.trackerConfig.bind(this); + } + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; + pair = async ({ + assetInfos + }: { + assetInfos: AssetInfo[]; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pair: { + asset_infos: assetInfos + } + }); + }; + pairs = async ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: AssetInfo[]; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pairs: { + limit, + start_after: startAfter + } + }); + }; + feeInfo = async ({ + pairType + }: { + pairType: PairType; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + fee_info: { + pair_type: pairType + } + }); + }; + blacklistedPairTypes = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + blacklisted_pair_types: {} + }); + }; + trackerConfig = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + tracker_config: {} + }); + }; +} +export interface FactoryInterface { + contractAddress: string; + sender: string; + updateConfig: ({ + coinRegistryAddress, + feeAddress, + generatorAddress, + tokenCodeId, + whitelistCodeId + }: { + coinRegistryAddress?: string; + feeAddress?: string; + generatorAddress?: string; + tokenCodeId?: number; + whitelistCodeId?: number; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + updateTrackerConfig: ({ + tokenFactoryAddr, + trackerCodeId + }: { + tokenFactoryAddr?: string; + trackerCodeId: number; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + updatePairConfig: ({ + config + }: { + config: PairConfig; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + createPair: ({ + assetInfos, + initParams, + pairType + }: { + assetInfos: AssetInfo[]; + initParams?: Binary; + pairType: PairType; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + deregister: ({ + assetInfos + }: { + assetInfos: AssetInfo[]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + dropOwnershipProposal: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + claimOwnership: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +} +export class FactoryClient implements FactoryInterface { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.updateConfig = this.updateConfig.bind(this); + this.updateTrackerConfig = this.updateTrackerConfig.bind(this); + this.updatePairConfig = this.updatePairConfig.bind(this); + this.createPair = this.createPair.bind(this); + this.deregister = this.deregister.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + } + updateConfig = async ({ + coinRegistryAddress, + feeAddress, + generatorAddress, + tokenCodeId, + whitelistCodeId + }: { + coinRegistryAddress?: string; + feeAddress?: string; + generatorAddress?: string; + tokenCodeId?: number; + whitelistCodeId?: number; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_config: { + coin_registry_address: coinRegistryAddress, + fee_address: feeAddress, + generator_address: generatorAddress, + token_code_id: tokenCodeId, + whitelist_code_id: whitelistCodeId + } + }, fee_, memo_, funds_); + }; + updateTrackerConfig = async ({ + tokenFactoryAddr, + trackerCodeId + }: { + tokenFactoryAddr?: string; + trackerCodeId: number; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_tracker_config: { + token_factory_addr: tokenFactoryAddr, + tracker_code_id: trackerCodeId + } + }, fee_, memo_, funds_); + }; + updatePairConfig = async ({ + config + }: { + config: PairConfig; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_pair_config: { + config + } + }, fee_, memo_, funds_); + }; + createPair = async ({ + assetInfos, + initParams, + pairType + }: { + assetInfos: AssetInfo[]; + initParams?: Binary; + pairType: PairType; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + create_pair: { + asset_infos: assetInfos, + init_params: initParams, + pair_type: pairType + } + }, fee_, memo_, funds_); + }; + deregister = async ({ + assetInfos + }: { + assetInfos: AssetInfo[]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + deregister: { + asset_infos: assetInfos + } + }, fee_, memo_, funds_); + }; + proposeNewOwner = async ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + propose_new_owner: { + expires_in: expiresIn, + owner + } + }, fee_, memo_, funds_); + }; + dropOwnershipProposal = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + drop_ownership_proposal: {} + }, fee_, memo_, funds_); + }; + claimOwnership = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + claim_ownership: {} + }, fee_, memo_, funds_); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Factory.message-composer.ts b/frontend/src/lib/generated/Factory.message-composer.ts new file mode 100644 index 000000000..1c6a5baa1 --- /dev/null +++ b/frontend/src/lib/generated/Factory.message-composer.ts @@ -0,0 +1,243 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { Coin } from "@interchainjs/types"; +import { EncodeObject } from "@interchainjs/cosmos-types"; +import { MsgExecuteContract } from "interchainjs/cosmwasm/wasm/v1/tx"; +import { toUtf8 } from "@interchainjs/encoding"; +import { PairType, InstantiateMsg, PairConfig, TrackerConfig, ExecuteMsg, AssetInfo, Addr, Binary, QueryMsg, ArrayOfPairType, ConfigResponse, FeeInfoResponse, PairInfo, PairsResponse } from "./Factory.types"; +export interface FactoryMsg { + contractAddress: string; + sender: string; + updateConfig: ({ + coinRegistryAddress, + feeAddress, + generatorAddress, + tokenCodeId, + whitelistCodeId + }: { + coinRegistryAddress?: string; + feeAddress?: string; + generatorAddress?: string; + tokenCodeId?: number; + whitelistCodeId?: number; + }, funds_?: Coin[]) => EncodeObject; + updateTrackerConfig: ({ + tokenFactoryAddr, + trackerCodeId + }: { + tokenFactoryAddr?: string; + trackerCodeId: number; + }, funds_?: Coin[]) => EncodeObject; + updatePairConfig: ({ + config + }: { + config: PairConfig; + }, funds_?: Coin[]) => EncodeObject; + createPair: ({ + assetInfos, + initParams, + pairType + }: { + assetInfos: AssetInfo[]; + initParams?: Binary; + pairType: PairType; + }, funds_?: Coin[]) => EncodeObject; + deregister: ({ + assetInfos + }: { + assetInfos: AssetInfo[]; + }, funds_?: Coin[]) => EncodeObject; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]) => EncodeObject; + dropOwnershipProposal: (funds_?: Coin[]) => EncodeObject; + claimOwnership: (funds_?: Coin[]) => EncodeObject; +} +export class FactoryMsgComposer implements FactoryMsg { + sender: string; + contractAddress: string; + constructor(sender: string, contractAddress: string) { + this.sender = sender; + this.contractAddress = contractAddress; + this.updateConfig = this.updateConfig.bind(this); + this.updateTrackerConfig = this.updateTrackerConfig.bind(this); + this.updatePairConfig = this.updatePairConfig.bind(this); + this.createPair = this.createPair.bind(this); + this.deregister = this.deregister.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + } + updateConfig = ({ + coinRegistryAddress, + feeAddress, + generatorAddress, + tokenCodeId, + whitelistCodeId + }: { + coinRegistryAddress?: string; + feeAddress?: string; + generatorAddress?: string; + tokenCodeId?: number; + whitelistCodeId?: number; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update_config: { + coin_registry_address: coinRegistryAddress, + fee_address: feeAddress, + generator_address: generatorAddress, + token_code_id: tokenCodeId, + whitelist_code_id: whitelistCodeId + } + })), + funds: funds_ + }) + }; + }; + updateTrackerConfig = ({ + tokenFactoryAddr, + trackerCodeId + }: { + tokenFactoryAddr?: string; + trackerCodeId: number; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update_tracker_config: { + token_factory_addr: tokenFactoryAddr, + tracker_code_id: trackerCodeId + } + })), + funds: funds_ + }) + }; + }; + updatePairConfig = ({ + config + }: { + config: PairConfig; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update_pair_config: { + config + } + })), + funds: funds_ + }) + }; + }; + createPair = ({ + assetInfos, + initParams, + pairType + }: { + assetInfos: AssetInfo[]; + initParams?: Binary; + pairType: PairType; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + create_pair: { + asset_infos: assetInfos, + init_params: initParams, + pair_type: pairType + } + })), + funds: funds_ + }) + }; + }; + deregister = ({ + assetInfos + }: { + assetInfos: AssetInfo[]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + deregister: { + asset_infos: assetInfos + } + })), + funds: funds_ + }) + }; + }; + proposeNewOwner = ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + propose_new_owner: { + expires_in: expiresIn, + owner + } + })), + funds: funds_ + }) + }; + }; + dropOwnershipProposal = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + drop_ownership_proposal: {} + })), + funds: funds_ + }) + }; + }; + claimOwnership = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + claim_ownership: {} + })), + funds: funds_ + }) + }; + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Factory.types.ts b/frontend/src/lib/generated/Factory.types.ts new file mode 100644 index 000000000..88db5e732 --- /dev/null +++ b/frontend/src/lib/generated/Factory.types.ts @@ -0,0 +1,130 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +export type PairType = { + xyk: {}; +} | { + stable: {}; +} | { + custom: string; +}; +export interface InstantiateMsg { + coin_registry_address: string; + fee_address?: string | null; + generator_address?: string | null; + owner: string; + pair_configs: PairConfig[]; + token_code_id: number; + tracker_config?: TrackerConfig | null; + whitelist_code_id: number; +} +export interface PairConfig { + code_id: number; + is_disabled?: boolean; + is_generator_disabled?: boolean; + maker_fee_bps: number; + pair_type: PairType; + permissioned?: boolean; + total_fee_bps: number; + whitelist?: string[] | null; +} +export interface TrackerConfig { + code_id: number; + token_factory_addr: string; +} +export type ExecuteMsg = { + update_config: { + coin_registry_address?: string | null; + fee_address?: string | null; + generator_address?: string | null; + token_code_id?: number | null; + whitelist_code_id?: number | null; + }; +} | { + update_tracker_config: { + token_factory_addr?: string | null; + tracker_code_id: number; + }; +} | { + update_pair_config: { + config: PairConfig; + }; +} | { + create_pair: { + asset_infos: AssetInfo[]; + init_params?: Binary | null; + pair_type: PairType; + }; +} | { + deregister: { + asset_infos: AssetInfo[]; + }; +} | { + propose_new_owner: { + expires_in: number; + owner: string; + }; +} | { + drop_ownership_proposal: {}; +} | { + claim_ownership: {}; +}; +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export type Addr = string; +export type Binary = string; +export type QueryMsg = { + config: {}; +} | { + pair: { + asset_infos: AssetInfo[]; + }; +} | { + pairs: { + limit?: number | null; + start_after?: AssetInfo[] | null; + }; +} | { + fee_info: { + pair_type: PairType; + }; +} | { + blacklisted_pair_types: {}; +} | { + tracker_config: {}; +}; +export type ArrayOfPairType = PairType[]; +export interface ConfigResponse { + coin_registry_address: Addr; + fee_address?: Addr | null; + generator_address?: Addr | null; + owner: Addr; + pair_configs: PairConfig[]; + token_code_id: number; + whitelist_code_id: number; +} +export interface FeeInfoResponse { + fee_address?: Addr | null; + maker_fee_bps: number; + total_fee_bps: number; +} +export interface PairInfo { + asset_infos: AssetInfo[]; + contract_addr: Addr; + liquidity_token: string; + pair_type: PairType; +} +export interface PairsResponse { + pairs: PairInfo[]; +} +export type FactoryExecuteMsg = ExecuteMsg; \ No newline at end of file diff --git a/frontend/src/lib/generated/Incentives.client.ts b/frontend/src/lib/generated/Incentives.client.ts new file mode 100644 index 000000000..536546037 --- /dev/null +++ b/frontend/src/lib/generated/Incentives.client.ts @@ -0,0 +1,538 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { ICosmWasmClient, ISigningCosmWasmClient } from "./baseClient"; +import { StdFee } from "@interchainjs/types"; +import { Uint128, Addr, AssetInfo, InstantiateMsg, IncentivizationFeeInfo, Coin, ExecuteMsg, GeneratorControllerUpdate, InputSchedule, Asset, QueryMsg, ArrayOfTupleOfStringAndUint128, ArrayOfAssetInfo, Config, Decimal256, ArrayOfScheduleResponse, ScheduleResponse, Boolean, ArrayOfString, ArrayOfAsset, RewardType, PoolInfoResponse, RewardInfo, ArrayOfRewardInfo } from "./Incentives.types"; +export interface IncentivesReadOnlyInterface { + contractAddress: string; + config: () => Promise; + deposit: ({ + lpToken, + user + }: { + lpToken: string; + user: string; + }) => Promise; + pendingRewards: ({ + lpToken, + user + }: { + lpToken: string; + user: string; + }) => Promise; + rewardInfo: ({ + lpToken + }: { + lpToken: string; + }) => Promise; + poolInfo: ({ + lpToken + }: { + lpToken: string; + }) => Promise; + poolStakers: ({ + limit, + lpToken, + startAfter + }: { + limit?: number; + lpToken: string; + startAfter?: string; + }) => Promise; + blockedTokensList: ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: AssetInfo; + }) => Promise; + isFeeExpected: ({ + lpToken, + reward + }: { + lpToken: string; + reward: string; + }) => Promise; + externalRewardSchedules: ({ + limit, + lpToken, + reward, + startAfter + }: { + limit?: number; + lpToken: string; + reward: string; + startAfter?: number; + }) => Promise; + listPools: ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: string; + }) => Promise; + activePools: () => Promise; +} +export class IncentivesQueryClient implements IncentivesReadOnlyInterface { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.config = this.config.bind(this); + this.deposit = this.deposit.bind(this); + this.pendingRewards = this.pendingRewards.bind(this); + this.rewardInfo = this.rewardInfo.bind(this); + this.poolInfo = this.poolInfo.bind(this); + this.poolStakers = this.poolStakers.bind(this); + this.blockedTokensList = this.blockedTokensList.bind(this); + this.isFeeExpected = this.isFeeExpected.bind(this); + this.externalRewardSchedules = this.externalRewardSchedules.bind(this); + this.listPools = this.listPools.bind(this); + this.activePools = this.activePools.bind(this); + } + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; + deposit = async ({ + lpToken, + user + }: { + lpToken: string; + user: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + deposit: { + lp_token: lpToken, + user + } + }); + }; + pendingRewards = async ({ + lpToken, + user + }: { + lpToken: string; + user: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pending_rewards: { + lp_token: lpToken, + user + } + }); + }; + rewardInfo = async ({ + lpToken + }: { + lpToken: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + reward_info: { + lp_token: lpToken + } + }); + }; + poolInfo = async ({ + lpToken + }: { + lpToken: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pool_info: { + lp_token: lpToken + } + }); + }; + poolStakers = async ({ + limit, + lpToken, + startAfter + }: { + limit?: number; + lpToken: string; + startAfter?: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pool_stakers: { + limit, + lp_token: lpToken, + start_after: startAfter + } + }); + }; + blockedTokensList = async ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: AssetInfo; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + blocked_tokens_list: { + limit, + start_after: startAfter + } + }); + }; + isFeeExpected = async ({ + lpToken, + reward + }: { + lpToken: string; + reward: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + is_fee_expected: { + lp_token: lpToken, + reward + } + }); + }; + externalRewardSchedules = async ({ + limit, + lpToken, + reward, + startAfter + }: { + limit?: number; + lpToken: string; + reward: string; + startAfter?: number; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + external_reward_schedules: { + limit, + lp_token: lpToken, + reward, + start_after: startAfter + } + }); + }; + listPools = async ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + list_pools: { + limit, + start_after: startAfter + } + }); + }; + activePools = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + active_pools: {} + }); + }; +} +export interface IncentivesInterface { + contractAddress: string; + sender: string; + setupPools: ({ + pools + }: { + pools: string[][]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + claimRewards: ({ + lpTokens + }: { + lpTokens: string[]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + deposit: ({ + recipient + }: { + recipient?: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + withdraw: ({ + amount, + lpToken + }: { + amount: Uint128; + lpToken: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + setTokensPerSecond: ({ + amount + }: { + amount: Uint128; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + incentivize: ({ + lpToken, + schedule + }: { + lpToken: string; + schedule: InputSchedule; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + incentivizeMany: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + removeRewardFromPool: ({ + bypassUpcomingSchedules, + lpToken, + receiver, + reward + }: { + bypassUpcomingSchedules: boolean; + lpToken: string; + receiver: string; + reward: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + claimOrphanedRewards: ({ + limit, + receiver + }: { + limit?: number; + receiver: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + updateConfig: ({ + generatorController, + guardian, + incentivizationFeeInfo, + tokenTransferGasLimit + }: { + generatorController?: GeneratorControllerUpdate; + guardian?: string; + incentivizationFeeInfo?: IncentivizationFeeInfo; + tokenTransferGasLimit?: number; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + updateBlockedTokenslist: ({ + add, + remove + }: { + add?: AssetInfo[]; + remove?: AssetInfo[]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + deactivatePool: ({ + lpToken + }: { + lpToken: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + deactivateBlockedPools: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + dropOwnershipProposal: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + claimOwnership: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +} +export class IncentivesClient implements IncentivesInterface { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.setupPools = this.setupPools.bind(this); + this.claimRewards = this.claimRewards.bind(this); + this.deposit = this.deposit.bind(this); + this.withdraw = this.withdraw.bind(this); + this.setTokensPerSecond = this.setTokensPerSecond.bind(this); + this.incentivize = this.incentivize.bind(this); + this.incentivizeMany = this.incentivizeMany.bind(this); + this.removeRewardFromPool = this.removeRewardFromPool.bind(this); + this.claimOrphanedRewards = this.claimOrphanedRewards.bind(this); + this.updateConfig = this.updateConfig.bind(this); + this.updateBlockedTokenslist = this.updateBlockedTokenslist.bind(this); + this.deactivatePool = this.deactivatePool.bind(this); + this.deactivateBlockedPools = this.deactivateBlockedPools.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + } + setupPools = async ({ + pools + }: { + pools: string[][]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + setup_pools: { + pools + } + }, fee_, memo_, funds_); + }; + claimRewards = async ({ + lpTokens + }: { + lpTokens: string[]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + claim_rewards: { + lp_tokens: lpTokens + } + }, fee_, memo_, funds_); + }; + deposit = async ({ + recipient + }: { + recipient?: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + deposit: { + recipient + } + }, fee_, memo_, funds_); + }; + withdraw = async ({ + amount, + lpToken + }: { + amount: Uint128; + lpToken: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + withdraw: { + amount, + lp_token: lpToken + } + }, fee_, memo_, funds_); + }; + setTokensPerSecond = async ({ + amount + }: { + amount: Uint128; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + set_tokens_per_second: { + amount + } + }, fee_, memo_, funds_); + }; + incentivize = async ({ + lpToken, + schedule + }: { + lpToken: string; + schedule: InputSchedule; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + incentivize: { + lp_token: lpToken, + schedule + } + }, fee_, memo_, funds_); + }; + incentivizeMany = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + incentivize_many: {} + }, fee_, memo_, funds_); + }; + removeRewardFromPool = async ({ + bypassUpcomingSchedules, + lpToken, + receiver, + reward + }: { + bypassUpcomingSchedules: boolean; + lpToken: string; + receiver: string; + reward: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + remove_reward_from_pool: { + bypass_upcoming_schedules: bypassUpcomingSchedules, + lp_token: lpToken, + receiver, + reward + } + }, fee_, memo_, funds_); + }; + claimOrphanedRewards = async ({ + limit, + receiver + }: { + limit?: number; + receiver: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + claim_orphaned_rewards: { + limit, + receiver + } + }, fee_, memo_, funds_); + }; + updateConfig = async ({ + generatorController, + guardian, + incentivizationFeeInfo, + tokenTransferGasLimit + }: { + generatorController?: GeneratorControllerUpdate; + guardian?: string; + incentivizationFeeInfo?: IncentivizationFeeInfo; + tokenTransferGasLimit?: number; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_config: { + generator_controller: generatorController, + guardian, + incentivization_fee_info: incentivizationFeeInfo, + token_transfer_gas_limit: tokenTransferGasLimit + } + }, fee_, memo_, funds_); + }; + updateBlockedTokenslist = async ({ + add, + remove + }: { + add?: AssetInfo[]; + remove?: AssetInfo[]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_blocked_tokenslist: { + add, + remove + } + }, fee_, memo_, funds_); + }; + deactivatePool = async ({ + lpToken + }: { + lpToken: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + deactivate_pool: { + lp_token: lpToken + } + }, fee_, memo_, funds_); + }; + deactivateBlockedPools = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + deactivate_blocked_pools: {} + }, fee_, memo_, funds_); + }; + proposeNewOwner = async ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + propose_new_owner: { + expires_in: expiresIn, + owner + } + }, fee_, memo_, funds_); + }; + dropOwnershipProposal = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + drop_ownership_proposal: {} + }, fee_, memo_, funds_); + }; + claimOwnership = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + claim_ownership: {} + }, fee_, memo_, funds_); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Incentives.message-composer.ts b/frontend/src/lib/generated/Incentives.message-composer.ts new file mode 100644 index 000000000..ef8fdede8 --- /dev/null +++ b/frontend/src/lib/generated/Incentives.message-composer.ts @@ -0,0 +1,437 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { EncodeObject } from "@interchainjs/cosmos-types"; +import { MsgExecuteContract } from "interchainjs/cosmwasm/wasm/v1/tx"; +import { toUtf8 } from "@interchainjs/encoding"; +import { Uint128, Addr, AssetInfo, InstantiateMsg, IncentivizationFeeInfo, Coin, ExecuteMsg, GeneratorControllerUpdate, InputSchedule, Asset, QueryMsg, ArrayOfTupleOfStringAndUint128, ArrayOfAssetInfo, Config, Decimal256, ArrayOfScheduleResponse, ScheduleResponse, Boolean, ArrayOfString, ArrayOfAsset, RewardType, PoolInfoResponse, RewardInfo, ArrayOfRewardInfo } from "./Incentives.types"; +export interface IncentivesMsg { + contractAddress: string; + sender: string; + setupPools: ({ + pools + }: { + pools: string[][]; + }, funds_?: Coin[]) => EncodeObject; + claimRewards: ({ + lpTokens + }: { + lpTokens: string[]; + }, funds_?: Coin[]) => EncodeObject; + deposit: ({ + recipient + }: { + recipient?: string; + }, funds_?: Coin[]) => EncodeObject; + withdraw: ({ + amount, + lpToken + }: { + amount: Uint128; + lpToken: string; + }, funds_?: Coin[]) => EncodeObject; + setTokensPerSecond: ({ + amount + }: { + amount: Uint128; + }, funds_?: Coin[]) => EncodeObject; + incentivize: ({ + lpToken, + schedule + }: { + lpToken: string; + schedule: InputSchedule; + }, funds_?: Coin[]) => EncodeObject; + incentivizeMany: (funds_?: Coin[]) => EncodeObject; + removeRewardFromPool: ({ + bypassUpcomingSchedules, + lpToken, + receiver, + reward + }: { + bypassUpcomingSchedules: boolean; + lpToken: string; + receiver: string; + reward: string; + }, funds_?: Coin[]) => EncodeObject; + claimOrphanedRewards: ({ + limit, + receiver + }: { + limit?: number; + receiver: string; + }, funds_?: Coin[]) => EncodeObject; + updateConfig: ({ + generatorController, + guardian, + incentivizationFeeInfo, + tokenTransferGasLimit + }: { + generatorController?: GeneratorControllerUpdate; + guardian?: string; + incentivizationFeeInfo?: IncentivizationFeeInfo; + tokenTransferGasLimit?: number; + }, funds_?: Coin[]) => EncodeObject; + updateBlockedTokenslist: ({ + add, + remove + }: { + add?: AssetInfo[]; + remove?: AssetInfo[]; + }, funds_?: Coin[]) => EncodeObject; + deactivatePool: ({ + lpToken + }: { + lpToken: string; + }, funds_?: Coin[]) => EncodeObject; + deactivateBlockedPools: (funds_?: Coin[]) => EncodeObject; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]) => EncodeObject; + dropOwnershipProposal: (funds_?: Coin[]) => EncodeObject; + claimOwnership: (funds_?: Coin[]) => EncodeObject; +} +export class IncentivesMsgComposer implements IncentivesMsg { + sender: string; + contractAddress: string; + constructor(sender: string, contractAddress: string) { + this.sender = sender; + this.contractAddress = contractAddress; + this.setupPools = this.setupPools.bind(this); + this.claimRewards = this.claimRewards.bind(this); + this.deposit = this.deposit.bind(this); + this.withdraw = this.withdraw.bind(this); + this.setTokensPerSecond = this.setTokensPerSecond.bind(this); + this.incentivize = this.incentivize.bind(this); + this.incentivizeMany = this.incentivizeMany.bind(this); + this.removeRewardFromPool = this.removeRewardFromPool.bind(this); + this.claimOrphanedRewards = this.claimOrphanedRewards.bind(this); + this.updateConfig = this.updateConfig.bind(this); + this.updateBlockedTokenslist = this.updateBlockedTokenslist.bind(this); + this.deactivatePool = this.deactivatePool.bind(this); + this.deactivateBlockedPools = this.deactivateBlockedPools.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + } + setupPools = ({ + pools + }: { + pools: string[][]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + setup_pools: { + pools + } + })), + funds: funds_ + }) + }; + }; + claimRewards = ({ + lpTokens + }: { + lpTokens: string[]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + claim_rewards: { + lp_tokens: lpTokens + } + })), + funds: funds_ + }) + }; + }; + deposit = ({ + recipient + }: { + recipient?: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + deposit: { + recipient + } + })), + funds: funds_ + }) + }; + }; + withdraw = ({ + amount, + lpToken + }: { + amount: Uint128; + lpToken: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + withdraw: { + amount, + lp_token: lpToken + } + })), + funds: funds_ + }) + }; + }; + setTokensPerSecond = ({ + amount + }: { + amount: Uint128; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + set_tokens_per_second: { + amount + } + })), + funds: funds_ + }) + }; + }; + incentivize = ({ + lpToken, + schedule + }: { + lpToken: string; + schedule: InputSchedule; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + incentivize: { + lp_token: lpToken, + schedule + } + })), + funds: funds_ + }) + }; + }; + incentivizeMany = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + incentivize_many: {} + })), + funds: funds_ + }) + }; + }; + removeRewardFromPool = ({ + bypassUpcomingSchedules, + lpToken, + receiver, + reward + }: { + bypassUpcomingSchedules: boolean; + lpToken: string; + receiver: string; + reward: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + remove_reward_from_pool: { + bypass_upcoming_schedules: bypassUpcomingSchedules, + lp_token: lpToken, + receiver, + reward + } + })), + funds: funds_ + }) + }; + }; + claimOrphanedRewards = ({ + limit, + receiver + }: { + limit?: number; + receiver: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + claim_orphaned_rewards: { + limit, + receiver + } + })), + funds: funds_ + }) + }; + }; + updateConfig = ({ + generatorController, + guardian, + incentivizationFeeInfo, + tokenTransferGasLimit + }: { + generatorController?: GeneratorControllerUpdate; + guardian?: string; + incentivizationFeeInfo?: IncentivizationFeeInfo; + tokenTransferGasLimit?: number; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update_config: { + generator_controller: generatorController, + guardian, + incentivization_fee_info: incentivizationFeeInfo, + token_transfer_gas_limit: tokenTransferGasLimit + } + })), + funds: funds_ + }) + }; + }; + updateBlockedTokenslist = ({ + add, + remove + }: { + add?: AssetInfo[]; + remove?: AssetInfo[]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update_blocked_tokenslist: { + add, + remove + } + })), + funds: funds_ + }) + }; + }; + deactivatePool = ({ + lpToken + }: { + lpToken: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + deactivate_pool: { + lp_token: lpToken + } + })), + funds: funds_ + }) + }; + }; + deactivateBlockedPools = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + deactivate_blocked_pools: {} + })), + funds: funds_ + }) + }; + }; + proposeNewOwner = ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + propose_new_owner: { + expires_in: expiresIn, + owner + } + })), + funds: funds_ + }) + }; + }; + dropOwnershipProposal = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + drop_ownership_proposal: {} + })), + funds: funds_ + }) + }; + }; + claimOwnership = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + claim_ownership: {} + })), + funds: funds_ + }) + }; + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Incentives.types.ts b/frontend/src/lib/generated/Incentives.types.ts new file mode 100644 index 000000000..6d3097b63 --- /dev/null +++ b/frontend/src/lib/generated/Incentives.types.ts @@ -0,0 +1,207 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +export type Uint128 = string; +export type Addr = string; +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export interface InstantiateMsg { + factory: string; + guardian?: string | null; + incentivization_fee_info?: IncentivizationFeeInfo | null; + owner: string; + reward_token: AssetInfo; +} +export interface IncentivizationFeeInfo { + fee: Coin; + fee_receiver: Addr; +} +export interface Coin { + amount: Uint128; + denom: string; + [k: string]: unknown; +} +export type ExecuteMsg = { + setup_pools: { + pools: [string, Uint128][]; + }; +} | { + claim_rewards: { + lp_tokens: string[]; + }; +} | { + deposit: { + recipient?: string | null; + }; +} | { + withdraw: { + amount: Uint128; + lp_token: string; + }; +} | { + set_tokens_per_second: { + amount: Uint128; + }; +} | { + incentivize: { + lp_token: string; + schedule: InputSchedule; + }; +} | { + incentivize_many: [string, InputSchedule][]; +} | { + remove_reward_from_pool: { + bypass_upcoming_schedules?: boolean; + lp_token: string; + receiver: string; + reward: string; + }; +} | { + claim_orphaned_rewards: { + limit?: number | null; + receiver: string; + }; +} | { + update_config: { + generator_controller?: GeneratorControllerUpdate & string; + guardian?: string | null; + incentivization_fee_info?: IncentivizationFeeInfo | null; + token_transfer_gas_limit?: number | null; + }; +} | { + update_blocked_tokenslist: { + add?: AssetInfo[]; + remove?: AssetInfo[]; + }; +} | { + deactivate_pool: { + lp_token: string; + }; +} | { + deactivate_blocked_pools: {}; +} | { + propose_new_owner: { + expires_in: number; + owner: string; + }; +} | { + drop_ownership_proposal: {}; +} | { + claim_ownership: {}; +}; +export type GeneratorControllerUpdate = { + set: string; +} | "unset" | "no_change"; +export interface InputSchedule { + duration_periods: number; + reward: Asset; +} +export interface Asset { + amount: Uint128; + info: AssetInfo; +} +export type QueryMsg = { + config: {}; +} | { + deposit: { + lp_token: string; + user: string; + }; +} | { + pending_rewards: { + lp_token: string; + user: string; + }; +} | { + reward_info: { + lp_token: string; + }; +} | { + pool_info: { + lp_token: string; + }; +} | { + pool_stakers: { + limit?: number | null; + lp_token: string; + start_after?: string | null; + }; +} | { + blocked_tokens_list: { + limit?: number | null; + start_after?: AssetInfo | null; + }; +} | { + is_fee_expected: { + lp_token: string; + reward: string; + }; +} | { + external_reward_schedules: { + limit?: number | null; + lp_token: string; + reward: string; + start_after?: number | null; + }; +} | { + list_pools: { + limit?: number | null; + start_after?: string | null; + }; +} | { + active_pools: {}; +}; +export type ArrayOfTupleOfStringAndUint128 = [string, Uint128][]; +export type ArrayOfAssetInfo = AssetInfo[]; +export interface Config { + factory: Addr; + generator_controller?: Addr | null; + guardian?: Addr | null; + incentivization_fee_info?: IncentivizationFeeInfo | null; + owner: Addr; + reward_per_second: Uint128; + reward_token: AssetInfo; + token_transfer_gas_limit?: number | null; + total_alloc_points: Uint128; +} +export type Decimal256 = string; +export type ArrayOfScheduleResponse = ScheduleResponse[]; +export interface ScheduleResponse { + end_ts: number; + rps: Decimal256; + start_ts: number; +} +export type Boolean = boolean; +export type ArrayOfString = string[]; +export type ArrayOfAsset = Asset[]; +export type RewardType = { + int: AssetInfo; +} | { + ext: { + info: AssetInfo; + next_update_ts: number; + }; +}; +export interface PoolInfoResponse { + last_update_ts: number; + rewards: RewardInfo[]; + total_lp: Uint128; +} +export interface RewardInfo { + index: Decimal256; + orphaned: Decimal256; + reward: RewardType; + rps: Decimal256; +} +export type ArrayOfRewardInfo = RewardInfo[]; +export type IncentivesExecuteMsg = ExecuteMsg; \ No newline at end of file diff --git a/frontend/src/lib/generated/NativeCoinRegistry.client.ts b/frontend/src/lib/generated/NativeCoinRegistry.client.ts new file mode 100644 index 000000000..d1ef6b538 --- /dev/null +++ b/frontend/src/lib/generated/NativeCoinRegistry.client.ts @@ -0,0 +1,167 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { ICosmWasmClient, ISigningCosmWasmClient } from "./baseClient"; +import { Coin, StdFee } from "@interchainjs/types"; +import { InstantiateMsg, ExecuteMsg, QueryMsg, Addr, Config, CoinResponse, ArrayOfCoinResponse } from "./NativeCoinRegistry.types"; +export interface NativeCoinRegistryReadOnlyInterface { + contractAddress: string; + config: () => Promise; + nativeToken: ({ + denom + }: { + denom: string; + }) => Promise; + nativeTokens: ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: string; + }) => Promise; +} +export class NativeCoinRegistryQueryClient implements NativeCoinRegistryReadOnlyInterface { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.config = this.config.bind(this); + this.nativeToken = this.nativeToken.bind(this); + this.nativeTokens = this.nativeTokens.bind(this); + } + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; + nativeToken = async ({ + denom + }: { + denom: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + native_token: { + denom + } + }); + }; + nativeTokens = async ({ + limit, + startAfter + }: { + limit?: number; + startAfter?: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + native_tokens: { + limit, + start_after: startAfter + } + }); + }; +} +export interface NativeCoinRegistryInterface { + contractAddress: string; + sender: string; + add: ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + register: ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + remove: ({ + nativeCoins + }: { + nativeCoins: string[]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + dropOwnershipProposal: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + claimOwnership: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +} +export class NativeCoinRegistryClient implements NativeCoinRegistryInterface { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.add = this.add.bind(this); + this.register = this.register.bind(this); + this.remove = this.remove.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + } + add = async ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + add: { + native_coins: nativeCoins + } + }, fee_, memo_, funds_); + }; + register = async ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + register: { + native_coins: nativeCoins + } + }, fee_, memo_, funds_); + }; + remove = async ({ + nativeCoins + }: { + nativeCoins: string[]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + remove: { + native_coins: nativeCoins + } + }, fee_, memo_, funds_); + }; + proposeNewOwner = async ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + propose_new_owner: { + expires_in: expiresIn, + owner + } + }, fee_, memo_, funds_); + }; + dropOwnershipProposal = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + drop_ownership_proposal: {} + }, fee_, memo_, funds_); + }; + claimOwnership = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + claim_ownership: {} + }, fee_, memo_, funds_); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/NativeCoinRegistry.message-composer.ts b/frontend/src/lib/generated/NativeCoinRegistry.message-composer.ts new file mode 100644 index 000000000..ac44f60ef --- /dev/null +++ b/frontend/src/lib/generated/NativeCoinRegistry.message-composer.ts @@ -0,0 +1,158 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { Coin } from "@interchainjs/types"; +import { EncodeObject } from "@interchainjs/cosmos-types"; +import { MsgExecuteContract } from "interchainjs/cosmwasm/wasm/v1/tx"; +import { toUtf8 } from "@interchainjs/encoding"; +import { InstantiateMsg, ExecuteMsg, QueryMsg, Addr, Config, CoinResponse, ArrayOfCoinResponse } from "./NativeCoinRegistry.types"; +export interface NativeCoinRegistryMsg { + contractAddress: string; + sender: string; + add: ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, funds_?: Coin[]) => EncodeObject; + register: ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, funds_?: Coin[]) => EncodeObject; + remove: ({ + nativeCoins + }: { + nativeCoins: string[]; + }, funds_?: Coin[]) => EncodeObject; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]) => EncodeObject; + dropOwnershipProposal: (funds_?: Coin[]) => EncodeObject; + claimOwnership: (funds_?: Coin[]) => EncodeObject; +} +export class NativeCoinRegistryMsgComposer implements NativeCoinRegistryMsg { + sender: string; + contractAddress: string; + constructor(sender: string, contractAddress: string) { + this.sender = sender; + this.contractAddress = contractAddress; + this.add = this.add.bind(this); + this.register = this.register.bind(this); + this.remove = this.remove.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + } + add = ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + add: { + native_coins: nativeCoins + } + })), + funds: funds_ + }) + }; + }; + register = ({ + nativeCoins + }: { + nativeCoins: string[][]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + register: { + native_coins: nativeCoins + } + })), + funds: funds_ + }) + }; + }; + remove = ({ + nativeCoins + }: { + nativeCoins: string[]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + remove: { + native_coins: nativeCoins + } + })), + funds: funds_ + }) + }; + }; + proposeNewOwner = ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + propose_new_owner: { + expires_in: expiresIn, + owner + } + })), + funds: funds_ + }) + }; + }; + dropOwnershipProposal = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + drop_ownership_proposal: {} + })), + funds: funds_ + }) + }; + }; + claimOwnership = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + claim_ownership: {} + })), + funds: funds_ + }) + }; + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/NativeCoinRegistry.types.ts b/frontend/src/lib/generated/NativeCoinRegistry.types.ts new file mode 100644 index 000000000..9dbb99cfc --- /dev/null +++ b/frontend/src/lib/generated/NativeCoinRegistry.types.ts @@ -0,0 +1,53 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +export interface InstantiateMsg { + owner: string; +} +export type ExecuteMsg = { + add: { + native_coins: [string, number][]; + }; +} | { + register: { + native_coins: [string, number][]; + }; +} | { + remove: { + native_coins: string[]; + }; +} | { + propose_new_owner: { + expires_in: number; + owner: string; + }; +} | { + drop_ownership_proposal: {}; +} | { + claim_ownership: {}; +}; +export type QueryMsg = { + config: {}; +} | { + native_token: { + denom: string; + }; +} | { + native_tokens: { + limit?: number | null; + start_after?: string | null; + }; +}; +export type Addr = string; +export interface Config { + owner: Addr; +} +export interface CoinResponse { + decimals: number; + denom: string; +} +export type ArrayOfCoinResponse = CoinResponse[]; +export type NativeCoinRegistryExecuteMsg = ExecuteMsg; \ No newline at end of file diff --git a/frontend/src/lib/generated/Oracle.client.ts b/frontend/src/lib/generated/Oracle.client.ts new file mode 100644 index 000000000..c9f97c83a --- /dev/null +++ b/frontend/src/lib/generated/Oracle.client.ts @@ -0,0 +1,63 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { ICosmWasmClient, ISigningCosmWasmClient } from "./baseClient"; +import { Coin, StdFee } from "@interchainjs/types"; +import { AssetInfo, Addr, InstantiateMsg, ExecuteMsg, QueryMsg, Uint128, MigrateMsg, Uint256, ArrayOfTupleOfAssetInfoAndUint256 } from "./Oracle.types"; +export interface OracleReadOnlyInterface { + contractAddress: string; + consult: ({ + amount, + token + }: { + amount: Uint128; + token: AssetInfo; + }) => Promise; +} +export class OracleQueryClient implements OracleReadOnlyInterface { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.consult = this.consult.bind(this); + } + consult = async ({ + amount, + token + }: { + amount: Uint128; + token: AssetInfo; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + consult: { + amount, + token + } + }); + }; +} +export interface OracleInterface { + contractAddress: string; + sender: string; + update: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +} +export class OracleClient implements OracleInterface { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.update = this.update.bind(this); + } + update = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update: {} + }, fee_, memo_, funds_); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Oracle.message-composer.ts b/frontend/src/lib/generated/Oracle.message-composer.ts new file mode 100644 index 000000000..d570497d2 --- /dev/null +++ b/frontend/src/lib/generated/Oracle.message-composer.ts @@ -0,0 +1,38 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { Coin } from "@interchainjs/types"; +import { EncodeObject } from "@interchainjs/cosmos-types"; +import { MsgExecuteContract } from "interchainjs/cosmwasm/wasm/v1/tx"; +import { toUtf8 } from "@interchainjs/encoding"; +import { AssetInfo, Addr, InstantiateMsg, ExecuteMsg, QueryMsg, Uint128, MigrateMsg, Uint256, ArrayOfTupleOfAssetInfoAndUint256 } from "./Oracle.types"; +export interface OracleMsg { + contractAddress: string; + sender: string; + update: (funds_?: Coin[]) => EncodeObject; +} +export class OracleMsgComposer implements OracleMsg { + sender: string; + contractAddress: string; + constructor(sender: string, contractAddress: string) { + this.sender = sender; + this.contractAddress = contractAddress; + this.update = this.update.bind(this); + } + update = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update: {} + })), + funds: funds_ + }) + }; + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Oracle.types.ts b/frontend/src/lib/generated/Oracle.types.ts new file mode 100644 index 000000000..78ef7b24e --- /dev/null +++ b/frontend/src/lib/generated/Oracle.types.ts @@ -0,0 +1,34 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export type Addr = string; +export interface InstantiateMsg { + asset_infos: AssetInfo[]; + factory_contract: string; +} +export type ExecuteMsg = { + update: {}; +}; +export type QueryMsg = { + consult: { + amount: Uint128; + token: AssetInfo; + }; +}; +export type Uint128 = string; +export interface MigrateMsg {} +export type Uint256 = string; +export type ArrayOfTupleOfAssetInfoAndUint256 = [AssetInfo, Uint256][]; +export type OracleExecuteMsg = ExecuteMsg; \ No newline at end of file diff --git a/frontend/src/lib/generated/Pair.client.ts b/frontend/src/lib/generated/Pair.client.ts new file mode 100644 index 000000000..851ba75e0 --- /dev/null +++ b/frontend/src/lib/generated/Pair.client.ts @@ -0,0 +1,391 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { ICosmWasmClient, ISigningCosmWasmClient } from "./baseClient"; +import { Coin, StdFee } from "@interchainjs/types"; +import { AssetInfo, Addr, Binary, PairType, InstantiateMsg, ExecuteMsg, Uint128, Decimal, Cw20ReceiveMsg, Asset, Empty, QueryMsg, Uint64, MigrateMsg, NullableUint128, ConfigResponse, CumulativePricesResponse, OracleObservation, PairInfo, PoolResponse, ReverseSimulationResponse, ArrayOfAsset, SimulationResponse } from "./Pair.types"; +export interface PairReadOnlyInterface { + contractAddress: string; + pair: () => Promise; + pool: () => Promise; + config: () => Promise; + share: ({ + amount + }: { + amount: Uint128; + }) => Promise; + simulation: ({ + askAssetInfo, + offerAsset + }: { + askAssetInfo?: AssetInfo; + offerAsset: Asset; + }) => Promise; + reverseSimulation: ({ + askAsset, + offerAssetInfo + }: { + askAsset: Asset; + offerAssetInfo?: AssetInfo; + }) => Promise; + cumulativePrices: () => Promise; + queryComputeD: () => Promise; + assetBalanceAt: ({ + assetInfo, + blockHeight + }: { + assetInfo: AssetInfo; + blockHeight: Uint64; + }) => Promise; + observe: ({ + secondsAgo + }: { + secondsAgo: number; + }) => Promise; + simulateWithdraw: ({ + lpAmount + }: { + lpAmount: Uint128; + }) => Promise; + simulateProvide: ({ + assets, + slippageTolerance + }: { + assets: Asset[]; + slippageTolerance?: Decimal; + }) => Promise; +} +export class PairQueryClient implements PairReadOnlyInterface { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.pair = this.pair.bind(this); + this.pool = this.pool.bind(this); + this.config = this.config.bind(this); + this.share = this.share.bind(this); + this.simulation = this.simulation.bind(this); + this.reverseSimulation = this.reverseSimulation.bind(this); + this.cumulativePrices = this.cumulativePrices.bind(this); + this.queryComputeD = this.queryComputeD.bind(this); + this.assetBalanceAt = this.assetBalanceAt.bind(this); + this.observe = this.observe.bind(this); + this.simulateWithdraw = this.simulateWithdraw.bind(this); + this.simulateProvide = this.simulateProvide.bind(this); + } + pair = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pair: {} + }); + }; + pool = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pool: {} + }); + }; + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; + share = async ({ + amount + }: { + amount: Uint128; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + share: { + amount + } + }); + }; + simulation = async ({ + askAssetInfo, + offerAsset + }: { + askAssetInfo?: AssetInfo; + offerAsset: Asset; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + simulation: { + ask_asset_info: askAssetInfo, + offer_asset: offerAsset + } + }); + }; + reverseSimulation = async ({ + askAsset, + offerAssetInfo + }: { + askAsset: Asset; + offerAssetInfo?: AssetInfo; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + reverse_simulation: { + ask_asset: askAsset, + offer_asset_info: offerAssetInfo + } + }); + }; + cumulativePrices = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + cumulative_prices: {} + }); + }; + queryComputeD = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + query_compute_d: {} + }); + }; + assetBalanceAt = async ({ + assetInfo, + blockHeight + }: { + assetInfo: AssetInfo; + blockHeight: Uint64; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + asset_balance_at: { + asset_info: assetInfo, + block_height: blockHeight + } + }); + }; + observe = async ({ + secondsAgo + }: { + secondsAgo: number; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + observe: { + seconds_ago: secondsAgo + } + }); + }; + simulateWithdraw = async ({ + lpAmount + }: { + lpAmount: Uint128; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + simulate_withdraw: { + lp_amount: lpAmount + } + }); + }; + simulateProvide = async ({ + assets, + slippageTolerance + }: { + assets: Asset[]; + slippageTolerance?: Decimal; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + simulate_provide: { + assets, + slippage_tolerance: slippageTolerance + } + }); + }; +} +export interface PairInterface { + contractAddress: string; + sender: string; + receive: ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + provideLiquidity: ({ + assets, + autoStake, + minLpToReceive, + receiver, + slippageTolerance + }: { + assets: Asset[]; + autoStake?: boolean; + minLpToReceive?: Uint128; + receiver?: string; + slippageTolerance?: Decimal; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + withdrawLiquidity: ({ + assets, + minAssetsToReceive + }: { + assets?: Asset[]; + minAssetsToReceive?: Asset[]; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + swap: ({ + askAssetInfo, + beliefPrice, + maxSpread, + offerAsset, + to + }: { + askAssetInfo?: AssetInfo; + beliefPrice?: Decimal; + maxSpread?: Decimal; + offerAsset: Asset; + to?: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + updateConfig: ({ + params + }: { + params: Binary; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + dropOwnershipProposal: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + claimOwnership: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + custom: (fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +} +export class PairClient implements PairInterface { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.receive = this.receive.bind(this); + this.provideLiquidity = this.provideLiquidity.bind(this); + this.withdrawLiquidity = this.withdrawLiquidity.bind(this); + this.swap = this.swap.bind(this); + this.updateConfig = this.updateConfig.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + this.custom = this.custom.bind(this); + } + receive = async ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + receive: { + amount, + msg, + sender + } + }, fee_, memo_, funds_); + }; + provideLiquidity = async ({ + assets, + autoStake, + minLpToReceive, + receiver, + slippageTolerance + }: { + assets: Asset[]; + autoStake?: boolean; + minLpToReceive?: Uint128; + receiver?: string; + slippageTolerance?: Decimal; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + provide_liquidity: { + assets, + auto_stake: autoStake, + min_lp_to_receive: minLpToReceive, + receiver, + slippage_tolerance: slippageTolerance + } + }, fee_, memo_, funds_); + }; + withdrawLiquidity = async ({ + assets, + minAssetsToReceive + }: { + assets?: Asset[]; + minAssetsToReceive?: Asset[]; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + withdraw_liquidity: { + assets, + min_assets_to_receive: minAssetsToReceive + } + }, fee_, memo_, funds_); + }; + swap = async ({ + askAssetInfo, + beliefPrice, + maxSpread, + offerAsset, + to + }: { + askAssetInfo?: AssetInfo; + beliefPrice?: Decimal; + maxSpread?: Decimal; + offerAsset: Asset; + to?: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + swap: { + ask_asset_info: askAssetInfo, + belief_price: beliefPrice, + max_spread: maxSpread, + offer_asset: offerAsset, + to + } + }, fee_, memo_, funds_); + }; + updateConfig = async ({ + params + }: { + params: Binary; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_config: { + params + } + }, fee_, memo_, funds_); + }; + proposeNewOwner = async ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + propose_new_owner: { + expires_in: expiresIn, + owner + } + }, fee_, memo_, funds_); + }; + dropOwnershipProposal = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + drop_ownership_proposal: {} + }, fee_, memo_, funds_); + }; + claimOwnership = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + claim_ownership: {} + }, fee_, memo_, funds_); + }; + custom = async (fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + custom: {} + }, fee_, memo_, funds_); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Pair.message-composer.ts b/frontend/src/lib/generated/Pair.message-composer.ts new file mode 100644 index 000000000..6b21d3351 --- /dev/null +++ b/frontend/src/lib/generated/Pair.message-composer.ts @@ -0,0 +1,278 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { Coin } from "@interchainjs/types"; +import { EncodeObject } from "@interchainjs/cosmos-types"; +import { MsgExecuteContract } from "interchainjs/cosmwasm/wasm/v1/tx"; +import { toUtf8 } from "@interchainjs/encoding"; +import { AssetInfo, Addr, Binary, PairType, InstantiateMsg, ExecuteMsg, Uint128, Decimal, Cw20ReceiveMsg, Asset, Empty, QueryMsg, Uint64, MigrateMsg, NullableUint128, ConfigResponse, CumulativePricesResponse, OracleObservation, PairInfo, PoolResponse, ReverseSimulationResponse, ArrayOfAsset, SimulationResponse } from "./Pair.types"; +export interface PairMsg { + contractAddress: string; + sender: string; + receive: ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, funds_?: Coin[]) => EncodeObject; + provideLiquidity: ({ + assets, + autoStake, + minLpToReceive, + receiver, + slippageTolerance + }: { + assets: Asset[]; + autoStake?: boolean; + minLpToReceive?: Uint128; + receiver?: string; + slippageTolerance?: Decimal; + }, funds_?: Coin[]) => EncodeObject; + withdrawLiquidity: ({ + assets, + minAssetsToReceive + }: { + assets?: Asset[]; + minAssetsToReceive?: Asset[]; + }, funds_?: Coin[]) => EncodeObject; + swap: ({ + askAssetInfo, + beliefPrice, + maxSpread, + offerAsset, + to + }: { + askAssetInfo?: AssetInfo; + beliefPrice?: Decimal; + maxSpread?: Decimal; + offerAsset: Asset; + to?: string; + }, funds_?: Coin[]) => EncodeObject; + updateConfig: ({ + params + }: { + params: Binary; + }, funds_?: Coin[]) => EncodeObject; + proposeNewOwner: ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]) => EncodeObject; + dropOwnershipProposal: (funds_?: Coin[]) => EncodeObject; + claimOwnership: (funds_?: Coin[]) => EncodeObject; + custom: (funds_?: Coin[]) => EncodeObject; +} +export class PairMsgComposer implements PairMsg { + sender: string; + contractAddress: string; + constructor(sender: string, contractAddress: string) { + this.sender = sender; + this.contractAddress = contractAddress; + this.receive = this.receive.bind(this); + this.provideLiquidity = this.provideLiquidity.bind(this); + this.withdrawLiquidity = this.withdrawLiquidity.bind(this); + this.swap = this.swap.bind(this); + this.updateConfig = this.updateConfig.bind(this); + this.proposeNewOwner = this.proposeNewOwner.bind(this); + this.dropOwnershipProposal = this.dropOwnershipProposal.bind(this); + this.claimOwnership = this.claimOwnership.bind(this); + this.custom = this.custom.bind(this); + } + receive = ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + receive: { + amount, + msg, + sender + } + })), + funds: funds_ + }) + }; + }; + provideLiquidity = ({ + assets, + autoStake, + minLpToReceive, + receiver, + slippageTolerance + }: { + assets: Asset[]; + autoStake?: boolean; + minLpToReceive?: Uint128; + receiver?: string; + slippageTolerance?: Decimal; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + provide_liquidity: { + assets, + auto_stake: autoStake, + min_lp_to_receive: minLpToReceive, + receiver, + slippage_tolerance: slippageTolerance + } + })), + funds: funds_ + }) + }; + }; + withdrawLiquidity = ({ + assets, + minAssetsToReceive + }: { + assets?: Asset[]; + minAssetsToReceive?: Asset[]; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + withdraw_liquidity: { + assets, + min_assets_to_receive: minAssetsToReceive + } + })), + funds: funds_ + }) + }; + }; + swap = ({ + askAssetInfo, + beliefPrice, + maxSpread, + offerAsset, + to + }: { + askAssetInfo?: AssetInfo; + beliefPrice?: Decimal; + maxSpread?: Decimal; + offerAsset: Asset; + to?: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + swap: { + ask_asset_info: askAssetInfo, + belief_price: beliefPrice, + max_spread: maxSpread, + offer_asset: offerAsset, + to + } + })), + funds: funds_ + }) + }; + }; + updateConfig = ({ + params + }: { + params: Binary; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + update_config: { + params + } + })), + funds: funds_ + }) + }; + }; + proposeNewOwner = ({ + expiresIn, + owner + }: { + expiresIn: number; + owner: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + propose_new_owner: { + expires_in: expiresIn, + owner + } + })), + funds: funds_ + }) + }; + }; + dropOwnershipProposal = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + drop_ownership_proposal: {} + })), + funds: funds_ + }) + }; + }; + claimOwnership = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + claim_ownership: {} + })), + funds: funds_ + }) + }; + }; + custom = (funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + custom: {} + })), + funds: funds_ + }) + }; + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Pair.types.ts b/frontend/src/lib/generated/Pair.types.ts new file mode 100644 index 000000000..924654b9e --- /dev/null +++ b/frontend/src/lib/generated/Pair.types.ts @@ -0,0 +1,168 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export type Addr = string; +export type Binary = string; +export type PairType = { + xyk: {}; +} | { + stable: {}; +} | { + custom: string; +}; +export interface InstantiateMsg { + asset_infos: AssetInfo[]; + factory_addr: string; + init_params?: Binary | null; + pair_type: PairType; + token_code_id: number; +} +export type ExecuteMsg = { + receive: Cw20ReceiveMsg; +} | { + provide_liquidity: { + assets: Asset[]; + auto_stake?: boolean | null; + min_lp_to_receive?: Uint128 | null; + receiver?: string | null; + slippage_tolerance?: Decimal | null; + }; +} | { + withdraw_liquidity: { + assets?: Asset[]; + min_assets_to_receive?: Asset[] | null; + }; +} | { + swap: { + ask_asset_info?: AssetInfo | null; + belief_price?: Decimal | null; + max_spread?: Decimal | null; + offer_asset: Asset; + to?: string | null; + }; +} | { + update_config: { + params: Binary; + }; +} | { + propose_new_owner: { + expires_in: number; + owner: string; + }; +} | { + drop_ownership_proposal: {}; +} | { + claim_ownership: {}; +} | { + custom: Empty; +}; +export type Uint128 = string; +export type Decimal = string; +export interface Cw20ReceiveMsg { + amount: Uint128; + msg: Binary; + sender: string; +} +export interface Asset { + amount: Uint128; + info: AssetInfo; +} +export interface Empty { + [k: string]: unknown; +} +export type QueryMsg = { + pair: {}; +} | { + pool: {}; +} | { + config: {}; +} | { + share: { + amount: Uint128; + }; +} | { + simulation: { + ask_asset_info?: AssetInfo | null; + offer_asset: Asset; + }; +} | { + reverse_simulation: { + ask_asset: Asset; + offer_asset_info?: AssetInfo | null; + }; +} | { + cumulative_prices: {}; +} | { + query_compute_d: {}; +} | { + asset_balance_at: { + asset_info: AssetInfo; + block_height: Uint64; + }; +} | { + observe: { + seconds_ago: number; + }; +} | { + simulate_withdraw: { + lp_amount: Uint128; + }; +} | { + simulate_provide: { + assets: Asset[]; + slippage_tolerance?: Decimal | null; + }; +}; +export type Uint64 = string; +export interface MigrateMsg {} +export type NullableUint128 = Uint128 | null; +export interface ConfigResponse { + block_time_last: number; + factory_addr: Addr; + owner: Addr; + params?: Binary | null; + tracker_addr?: Addr | null; +} +export interface CumulativePricesResponse { + assets: Asset[]; + cumulative_prices: [AssetInfo, AssetInfo, Uint128][]; + total_share: Uint128; +} +export interface OracleObservation { + price: Decimal; + timestamp: number; +} +export interface PairInfo { + asset_infos: AssetInfo[]; + contract_addr: Addr; + liquidity_token: string; + pair_type: PairType; +} +export interface PoolResponse { + assets: Asset[]; + total_share: Uint128; +} +export interface ReverseSimulationResponse { + commission_amount: Uint128; + offer_amount: Uint128; + spread_amount: Uint128; +} +export type ArrayOfAsset = Asset[]; +export interface SimulationResponse { + commission_amount: Uint128; + return_amount: Uint128; + spread_amount: Uint128; +} +export type PairExecuteMsg = ExecuteMsg; \ No newline at end of file diff --git a/frontend/src/lib/generated/Router.client.ts b/frontend/src/lib/generated/Router.client.ts new file mode 100644 index 000000000..34d4d835a --- /dev/null +++ b/frontend/src/lib/generated/Router.client.ts @@ -0,0 +1,176 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { ICosmWasmClient, ISigningCosmWasmClient } from "./baseClient"; +import { Coin, StdFee } from "@interchainjs/types"; +import { InstantiateMsg, ExecuteMsg, Uint128, Binary, Decimal, SwapOperation, AssetInfo, Addr, Cw20ReceiveMsg, QueryMsg, ConfigResponse, SimulateSwapOperationsResponse } from "./Router.types"; +export interface RouterReadOnlyInterface { + contractAddress: string; + config: () => Promise; + simulateSwapOperations: ({ + offerAmount, + operations + }: { + offerAmount: Uint128; + operations: SwapOperation[]; + }) => Promise; + reverseSimulateSwapOperations: ({ + askAmount, + operations + }: { + askAmount: Uint128; + operations: SwapOperation[]; + }) => Promise; +} +export class RouterQueryClient implements RouterReadOnlyInterface { + client: ICosmWasmClient; + contractAddress: string; + constructor(client: ICosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.config = this.config.bind(this); + this.simulateSwapOperations = this.simulateSwapOperations.bind(this); + this.reverseSimulateSwapOperations = this.reverseSimulateSwapOperations.bind(this); + } + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; + simulateSwapOperations = async ({ + offerAmount, + operations + }: { + offerAmount: Uint128; + operations: SwapOperation[]; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + simulate_swap_operations: { + offer_amount: offerAmount, + operations + } + }); + }; + reverseSimulateSwapOperations = async ({ + askAmount, + operations + }: { + askAmount: Uint128; + operations: SwapOperation[]; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + reverse_simulate_swap_operations: { + ask_amount: askAmount, + operations + } + }); + }; +} +export interface RouterInterface { + contractAddress: string; + sender: string; + receive: ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + executeSwapOperations: ({ + maxSpread, + minimumReceive, + operations, + to + }: { + maxSpread?: Decimal; + minimumReceive?: Uint128; + operations: SwapOperation[]; + to?: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; + executeSwapOperation: ({ + maxSpread, + operation, + single, + to + }: { + maxSpread?: Decimal; + operation: SwapOperation; + single: boolean; + to?: string; + }, fee_?: number | StdFee | "auto", memo_?: string, funds_?: Coin[]) => Promise; +} +export class RouterClient implements RouterInterface { + client: ISigningCosmWasmClient; + sender: string; + contractAddress: string; + constructor(client: ISigningCosmWasmClient, sender: string, contractAddress: string) { + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.receive = this.receive.bind(this); + this.executeSwapOperations = this.executeSwapOperations.bind(this); + this.executeSwapOperation = this.executeSwapOperation.bind(this); + } + receive = async ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + receive: { + amount, + msg, + sender + } + }, fee_, memo_, funds_); + }; + executeSwapOperations = async ({ + maxSpread, + minimumReceive, + operations, + to + }: { + maxSpread?: Decimal; + minimumReceive?: Uint128; + operations: SwapOperation[]; + to?: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + execute_swap_operations: { + max_spread: maxSpread, + minimum_receive: minimumReceive, + operations, + to + } + }, fee_, memo_, funds_); + }; + executeSwapOperation = async ({ + maxSpread, + operation, + single, + to + }: { + maxSpread?: Decimal; + operation: SwapOperation; + single: boolean; + to?: string; + }, fee_: number | StdFee | "auto" = "auto", memo_?: string, funds_?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + execute_swap_operation: { + max_spread: maxSpread, + operation, + single, + to + } + }, fee_, memo_, funds_); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Router.message-composer.ts b/frontend/src/lib/generated/Router.message-composer.ts new file mode 100644 index 000000000..080214241 --- /dev/null +++ b/frontend/src/lib/generated/Router.message-composer.ts @@ -0,0 +1,138 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import { Coin } from "@interchainjs/types"; +import { EncodeObject } from "@interchainjs/cosmos-types"; +import { MsgExecuteContract } from "interchainjs/cosmwasm/wasm/v1/tx"; +import { toUtf8 } from "@interchainjs/encoding"; +import { InstantiateMsg, ExecuteMsg, Uint128, Binary, Decimal, SwapOperation, AssetInfo, Addr, Cw20ReceiveMsg, QueryMsg, ConfigResponse, SimulateSwapOperationsResponse } from "./Router.types"; +export interface RouterMsg { + contractAddress: string; + sender: string; + receive: ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, funds_?: Coin[]) => EncodeObject; + executeSwapOperations: ({ + maxSpread, + minimumReceive, + operations, + to + }: { + maxSpread?: Decimal; + minimumReceive?: Uint128; + operations: SwapOperation[]; + to?: string; + }, funds_?: Coin[]) => EncodeObject; + executeSwapOperation: ({ + maxSpread, + operation, + single, + to + }: { + maxSpread?: Decimal; + operation: SwapOperation; + single: boolean; + to?: string; + }, funds_?: Coin[]) => EncodeObject; +} +export class RouterMsgComposer implements RouterMsg { + sender: string; + contractAddress: string; + constructor(sender: string, contractAddress: string) { + this.sender = sender; + this.contractAddress = contractAddress; + this.receive = this.receive.bind(this); + this.executeSwapOperations = this.executeSwapOperations.bind(this); + this.executeSwapOperation = this.executeSwapOperation.bind(this); + } + receive = ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + receive: { + amount, + msg, + sender + } + })), + funds: funds_ + }) + }; + }; + executeSwapOperations = ({ + maxSpread, + minimumReceive, + operations, + to + }: { + maxSpread?: Decimal; + minimumReceive?: Uint128; + operations: SwapOperation[]; + to?: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + execute_swap_operations: { + max_spread: maxSpread, + minimum_receive: minimumReceive, + operations, + to + } + })), + funds: funds_ + }) + }; + }; + executeSwapOperation = ({ + maxSpread, + operation, + single, + to + }: { + maxSpread?: Decimal; + operation: SwapOperation; + single: boolean; + to?: string; + }, funds_?: Coin[]): EncodeObject => { + return { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: MsgExecuteContract.fromPartial({ + sender: this.sender, + contract: this.contractAddress, + msg: toUtf8(JSON.stringify({ + execute_swap_operation: { + max_spread: maxSpread, + operation, + single, + to + } + })), + funds: funds_ + }) + }; + }; +} \ No newline at end of file diff --git a/frontend/src/lib/generated/Router.types.ts b/frontend/src/lib/generated/Router.types.ts new file mode 100644 index 000000000..936c1a24d --- /dev/null +++ b/frontend/src/lib/generated/Router.types.ts @@ -0,0 +1,75 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +export interface InstantiateMsg { + astroport_factory: string; +} +export type ExecuteMsg = { + receive: Cw20ReceiveMsg; +} | { + execute_swap_operations: { + max_spread?: Decimal | null; + minimum_receive?: Uint128 | null; + operations: SwapOperation[]; + to?: string | null; + }; +} | { + execute_swap_operation: { + max_spread?: Decimal | null; + operation: SwapOperation; + single: boolean; + to?: string | null; + }; +}; +export type Uint128 = string; +export type Binary = string; +export type Decimal = string; +export type SwapOperation = { + native_swap: { + ask_denom: string; + offer_denom: string; + }; +} | { + astro_swap: { + ask_asset_info: AssetInfo; + offer_asset_info: AssetInfo; + }; +}; +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export type Addr = string; +export interface Cw20ReceiveMsg { + amount: Uint128; + msg: Binary; + sender: string; +} +export type QueryMsg = { + config: {}; +} | { + simulate_swap_operations: { + offer_amount: Uint128; + operations: SwapOperation[]; + }; +} | { + reverse_simulate_swap_operations: { + ask_amount: Uint128; + operations: SwapOperation[]; + }; +}; +export interface ConfigResponse { + astroport_factory: string; +} +export interface SimulateSwapOperationsResponse { + amount: Uint128; +} +export type RouterExecuteMsg = ExecuteMsg; \ No newline at end of file diff --git a/frontend/src/lib/generated/baseClient.ts b/frontend/src/lib/generated/baseClient.ts new file mode 100644 index 000000000..d66400722 --- /dev/null +++ b/frontend/src/lib/generated/baseClient.ts @@ -0,0 +1,200 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + + +import { StdFee, Coin } from '@interchainjs/types'; +import { DirectSigner } from '@interchainjs/cosmos'; +import { getSmartContractState } from 'interchainjs/cosmwasm/wasm/v1/query.rpc.func'; +import { executeContract } from 'interchainjs/cosmwasm/wasm/v1/tx.rpc.func'; +import { QuerySmartContractStateRequest, QuerySmartContractStateResponse } from 'interchainjs/cosmwasm/wasm/v1/query'; +import { MsgExecuteContract } from 'interchainjs/cosmwasm/wasm/v1/tx'; +import { Chain } from '@chain-registry/v2-types'; + +// Encoding utility functions +const fromUint8Array = (uint8Array: Uint8Array): T => { + const text = new TextDecoder().decode(uint8Array); + return JSON.parse(text); +}; + +const toUint8Array = (obj: any): Uint8Array => { + const text = JSON.stringify(obj); + return new TextEncoder().encode(text); +}; + +// Chain registry configuration +// The amount under gasPrice represents gas price per unit +export interface ChainConfig { + chain?: Chain; + gasPrice?: { + denom: string; + amount: string; + }; +} + +// Gas fee calculation utilities +export const calculateGasFromChain = (chain: Chain, gasAmount: string): StdFee => { + try { + const feeTokens = chain.fees?.feeTokens; + + if (feeTokens && feeTokens.length > 0) { + const primaryToken = feeTokens[0]; + // v2 chain-registry uses camelCase: averageGasPrice, lowGasPrice, fixedMinGasPrice + const gasPrice = primaryToken.averageGasPrice || primaryToken.lowGasPrice || primaryToken.fixedMinGasPrice || 0.025; + const gasAmountNum = parseInt(gasAmount); + const feeAmount = Math.ceil(gasAmountNum * gasPrice).toString(); + + return { + amount: [{ + denom: primaryToken.denom, + amount: feeAmount + }], + gas: gasAmount + }; + } + } catch (error) { + console.warn('Failed to calculate gas from chain registry:', error); + } + + // Fallback to default + return { amount: [], gas: gasAmount }; +}; + +// Default gas amount - users can easily change this +export let DEFAULT_GAS_AMOUNT = '200000'; + +// Allow users to set their preferred default gas amount +export const setDefaultGasAmount = (gasAmount: string): void => { + DEFAULT_GAS_AMOUNT = gasAmount; +}; + +// Get current default gas amount +export const getDefaultGasAmount = (): string => DEFAULT_GAS_AMOUNT; + +export const getAutoGasFee = (chainConfig?: ChainConfig): StdFee => { + const gasAmount = DEFAULT_GAS_AMOUNT; + + if (chainConfig?.chain) { + return calculateGasFromChain(chainConfig.chain, gasAmount); + } + + if (chainConfig?.gasPrice) { + const gasAmountNum = parseInt(gasAmount); + const gasPriceNum = parseFloat(chainConfig.gasPrice.amount); + const feeAmount = Math.ceil(gasAmountNum * gasPriceNum).toString(); + + return { + amount: [{ + denom: chainConfig.gasPrice.denom, + amount: feeAmount + }], + gas: gasAmount + }; + } + + // Fallback: no fee tokens, just gas amount + return { amount: [], gas: gasAmount }; +}; + +// InterchainJS interfaces for CosmWasm clients +export interface ICosmWasmClient { + queryContractSmart(contractAddr: string, query: any): Promise; +} + +export interface ISigningCosmWasmClient extends ICosmWasmClient { + execute( + sender: string, + contractAddress: string, + msg: any, + fee?: number | StdFee | "auto", + memo?: string, + funds?: Coin[], + chainConfig?: ChainConfig + ): Promise; +} + +export interface ISigningClient { + signAndBroadcast( + signerAddress: string, + messages: any[], + fee: number | StdFee | "auto", + memo?: string + ): Promise; +} + +// Helper functions to create InterchainJS clients +export function getCosmWasmClient(rpcEndpoint: string): ICosmWasmClient { + return { + queryContractSmart: async (contractAddr: string, query: any) => { + // Create the request object + const request: QuerySmartContractStateRequest = { + address: contractAddr, + queryData: toUint8Array(query) + }; + + // Execute the query using InterchainJS + const response: QuerySmartContractStateResponse = await getSmartContractState(rpcEndpoint, request); + + // Parse and return the result + return fromUint8Array(response.data); + }, + }; +} + +export function getSigningCosmWasmClient(signingClient: DirectSigner, rpcEndpoint?: string): ISigningCosmWasmClient { + return { + queryContractSmart: async (contractAddr: string, query: any) => { + if (!rpcEndpoint) { + throw new Error('rpcEndpoint is required for queryContractSmart in signing client'); + } + const request: QuerySmartContractStateRequest = { + address: contractAddr, + queryData: toUint8Array(query) + }; + const response: QuerySmartContractStateResponse = await getSmartContractState(rpcEndpoint, request); + return fromUint8Array(response.data); + }, + execute: async ( + sender: string, + contractAddress: string, + msg: any, + fee?: number | StdFee | "auto", + memo?: string, + funds?: Coin[], + chainConfig?: ChainConfig + ) => { + // Handle fee conversion + let finalFee: StdFee; + if (typeof fee === 'number') { + finalFee = { amount: [], gas: fee.toString() }; + } else if (fee === 'auto') { + finalFee = getAutoGasFee(chainConfig); + } else if (fee) { + finalFee = fee; + } else { + finalFee = getAutoGasFee(chainConfig); + } + + // Create the message object + const message: MsgExecuteContract = { + sender, + contract: contractAddress, + msg: toUint8Array(msg), + funds: funds || [] + }; + + // Execute the transaction using InterchainJS + const result = await executeContract( + signingClient as any, + sender, + message, + finalFee, + memo || '' + ); + + return result; + }, + }; +} diff --git a/frontend/src/lib/generated/index.ts b/frontend/src/lib/generated/index.ts new file mode 100644 index 000000000..7bac10fb5 --- /dev/null +++ b/frontend/src/lib/generated/index.ts @@ -0,0 +1,60 @@ +/** +* This file was automatically generated by @cosmwasm/ts-codegen@1.14.1. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @cosmwasm/ts-codegen generate command to regenerate this file. +*/ + +import * as _0 from "./Factory.types"; +import * as _1 from "./Factory.client"; +import * as _2 from "./Factory.message-composer"; +import * as _3 from "./Pair.types"; +import * as _4 from "./Pair.client"; +import * as _5 from "./Pair.message-composer"; +import * as _6 from "./Router.types"; +import * as _7 from "./Router.client"; +import * as _8 from "./Router.message-composer"; +import * as _9 from "./Incentives.types"; +import * as _10 from "./Incentives.client"; +import * as _11 from "./Incentives.message-composer"; +import * as _12 from "./Oracle.types"; +import * as _13 from "./Oracle.client"; +import * as _14 from "./Oracle.message-composer"; +import * as _15 from "./NativeCoinRegistry.types"; +import * as _16 from "./NativeCoinRegistry.client"; +import * as _17 from "./NativeCoinRegistry.message-composer"; +import * as _18 from "./baseClient"; +export namespace contracts { + export const Factory = { + ..._0, + ..._1, + ..._2 + }; + export const Pair = { + ..._3, + ..._4, + ..._5 + }; + export const Router = { + ..._6, + ..._7, + ..._8 + }; + export const Incentives = { + ..._9, + ..._10, + ..._11 + }; + export const Oracle = { + ..._12, + ..._13, + ..._14 + }; + export const NativeCoinRegistry = { + ..._15, + ..._16, + ..._17 + }; + export const baseClient = { + ..._18 + }; +} \ No newline at end of file diff --git a/frontend/src/lib/incentives.test.ts b/frontend/src/lib/incentives.test.ts new file mode 100644 index 000000000..44f2d3c04 --- /dev/null +++ b/frontend/src/lib/incentives.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryPool } from "../config/registry"; +import { createClaimRewardsMessage, createStakeLpExecute, createUnstakeLpMessage, queryIncentivesPoolState, totalRewardRps } from "./incentives"; + +const pool: RegistryPool = { + id: "juno-token", + label: "JUNO / TOKEN", + pair: "juno1pair", + lpToken: "factory/juno1pair/astroport/share", + type: "xyk", + feeBps: 30, + enabled: true, + status: "active", + explorer: "https://ping.pub/juno/wasm/contract/juno1pair", + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6 }, + { kind: "native", id: "factory/pair/token", symbol: "TOKEN", decimals: 6 }, + ], +}; + +describe("incentives helpers", () => { + it("returns a safe empty state when incentives are not configured", async () => { + await expect(queryIncentivesPoolState(pool, "juno1wallet", "")).resolves.toEqual({ + configured: false, + lpToken: pool.lpToken, + pendingRewards: [], + rewardInfo: [], + }); + }); + + it("builds stake, unstake, and claim execute payloads without broadcasting", () => { + expect(createStakeLpExecute(pool, "1230000")).toEqual({ + msg: { deposit: { recipient: undefined } }, + funds: [{ denom: pool.lpToken, amount: "1230000" }], + }); + expect(createUnstakeLpMessage(pool, "420000")).toEqual({ withdraw: { lp_token: pool.lpToken, amount: "420000" } }); + expect(createClaimRewardsMessage(pool)).toEqual({ claim_rewards: { lp_tokens: [pool.lpToken] } }); + }); + + it("validates positive amounts for stake and unstake", () => { + expect(() => createStakeLpExecute(pool, "0")).toThrow(/positive stake amount/i); + expect(() => createUnstakeLpMessage(pool, "abc")).toThrow(/positive unstake amount/i); + }); + + it("sums active reward rates without fabricating APR", () => { + expect(totalRewardRps([ + { index: "0", orphaned: "0", rps: "1.5", reward: { int: { native_token: { denom: "ujuno" } } } }, + { index: "0", orphaned: "0", rps: "2.25", reward: { ext: { info: { native_token: { denom: "factory/reward" } }, next_update_ts: 10 } } }, + ])).toBe(3.75); + expect(totalRewardRps([])).toBeUndefined(); + }); +}); diff --git a/frontend/src/lib/incentives.ts b/frontend/src/lib/incentives.ts new file mode 100644 index 000000000..81371c24e --- /dev/null +++ b/frontend/src/lib/incentives.ts @@ -0,0 +1,90 @@ +import type { Coin } from "@cosmjs/stargate"; +import { dexRegistry, type RegistryPool } from "../config/registry"; +import { isE2EMode } from "../e2e/mocks"; +import type { Asset, ExecuteMsg, PoolInfoResponse, RewardInfo } from "./generated/Incentives.types"; +import { queryContractSmart } from "./astroport/queries"; + +export type IncentivesPoolState = { + configured: boolean; + contractAddress?: string; + lpToken: string; + stakedAmount?: string; + pendingRewards: Asset[]; + rewardInfo: RewardInfo[]; + poolInfo?: PoolInfoResponse; + queryError?: string; +}; + +export function getIncentivesContractAddress(): string | undefined { + return dexRegistry.incentives || undefined; +} + +export async function queryIncentivesPoolState(pool: RegistryPool, user?: string, incentivesAddress = getIncentivesContractAddress()): Promise { + if (!incentivesAddress) { + return { configured: false, lpToken: pool.lpToken, pendingRewards: [], rewardInfo: [] }; + } + + if (isE2EMode()) { + return { + configured: true, + contractAddress: incentivesAddress, + lpToken: pool.lpToken, + stakedAmount: user ? "5000000000" : undefined, + pendingRewards: [{ info: { native_token: { denom: pool.assets[0].id } }, amount: "1230000" }], + rewardInfo: [{ reward: { int: { native_token: { denom: pool.assets[0].id } } }, rps: "42", index: "0", orphaned: "0" } as RewardInfo], + }; + } + + const [poolInfoResult, rewardInfoResult, depositResult, pendingResult] = await Promise.allSettled([ + queryContractSmart(incentivesAddress, { pool_info: { lp_token: pool.lpToken } }), + queryContractSmart(incentivesAddress, { reward_info: { lp_token: pool.lpToken } }), + user ? queryContractSmart(incentivesAddress, { deposit: { lp_token: pool.lpToken, user } }) : Promise.resolve(undefined), + user ? queryContractSmart(incentivesAddress, { pending_rewards: { lp_token: pool.lpToken, user } }) : Promise.resolve([]), + ]); + + const queryError = [poolInfoResult, rewardInfoResult, depositResult, pendingResult] + .find((result) => result.status === "rejected") as PromiseRejectedResult | undefined; + + return { + configured: true, + contractAddress: incentivesAddress, + lpToken: pool.lpToken, + poolInfo: poolInfoResult.status === "fulfilled" ? poolInfoResult.value : undefined, + rewardInfo: rewardInfoResult.status === "fulfilled" ? rewardInfoResult.value : [], + stakedAmount: depositResult.status === "fulfilled" ? depositResult.value : undefined, + pendingRewards: pendingResult.status === "fulfilled" ? pendingResult.value : [], + queryError: queryError ? errorMessage(queryError.reason) : undefined, + }; +} + +export function createStakeLpExecute(pool: RegistryPool, amount: string, recipient?: string): { msg: ExecuteMsg; funds: Coin[] } { + assertPositiveBaseAmount(amount, "stake amount"); + return { + msg: { deposit: { recipient } }, + funds: [{ denom: pool.lpToken, amount }], + }; +} + +export function createUnstakeLpMessage(pool: RegistryPool, amount: string): ExecuteMsg { + assertPositiveBaseAmount(amount, "unstake amount"); + return { withdraw: { lp_token: pool.lpToken, amount } }; +} + +export function createClaimRewardsMessage(pool: RegistryPool): ExecuteMsg { + if (!pool.lpToken) throw new Error("LP token is required to claim rewards"); + return { claim_rewards: { lp_tokens: [pool.lpToken] } }; +} + +export function totalRewardRps(rewardInfo: RewardInfo[]): number | undefined { + const values = rewardInfo.map((reward) => Number(reward.rps)).filter((value) => Number.isFinite(value) && value > 0); + if (values.length === 0) return undefined; + return values.reduce((sum, value) => sum + value, 0); +} + +function assertPositiveBaseAmount(amount: string, label: string) { + if (!/^\d+$/.test(amount) || BigInt(amount) <= 0n) throw new Error(`Enter a positive ${label}`); +} + +function errorMessage(error: unknown) { + return error instanceof Error ? error.message : String(error); +} diff --git a/frontend/src/lib/indexer/client.test.ts b/frontend/src/lib/indexer/client.test.ts new file mode 100644 index 000000000..27a38d689 --- /dev/null +++ b/frontend/src/lib/indexer/client.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from "vitest"; +import { createIndexerClient } from "./client"; + +describe("indexer typed client", () => { + it("fetches paginated pool metrics from /pools", async () => { + const fetcher = vi.fn(async (url: string) => new Response(JSON.stringify({ data: [], pagination: { limit: 10, nextCursor: null } }), { status: 200 })) as unknown as typeof fetch; + const client = createIndexerClient({ baseUrl: "https://indexer.example/", fetcher }); + const pools = await client.pools({ limit: 10 }); + expect(fetcher).toHaveBeenCalledWith("https://indexer.example/pools?limit=10", undefined); + expect(pools.pagination.limit).toBe(10); + }); + + it("fetches USD prices from /prices", async () => { + const fetcher = vi.fn(async () => new Response(JSON.stringify({ data: [{ asset: "ujuno", priceUsd: 1.25, source: "stored", status: "fresh", stale: false, observedAt: "2026-07-02T00:00:00.000Z", ageMs: 0, isMock: false }] }), { status: 200 })) as unknown as typeof fetch; + const client = createIndexerClient({ baseUrl: "https://indexer.example/", fetcher }); + const prices = await client.prices(["ujuno", "ibc/mock"]); + expect(fetcher).toHaveBeenCalledWith("https://indexer.example/prices?assets=ujuno%2Cibc%2Fmock", undefined); + expect(prices.data[0].priceUsd).toBe(1.25); + }); + + it("fetches pool candles with interval and range filters", async () => { + const fetcher = vi.fn(async () => new Response(JSON.stringify({ data: [], pagination: { limit: 25, nextCursor: null }, meta: { interval: "1h", isMock: false } }), { status: 200 })) as unknown as typeof fetch; + const client = createIndexerClient({ baseUrl: "https://indexer.example/", fetcher }); + const candles = await client.poolCandles("juno1pool", { interval: "1h", from: "2026-07-01T00:00:00.000Z", to: "2026-07-02T00:00:00.000Z", baseAsset: "ujuno", quoteAsset: "ibc/usdc", limit: 25 }); + expect(fetcher).toHaveBeenCalledWith("https://indexer.example/pools/juno1pool/candles?interval=1h&from=2026-07-01T00%3A00%3A00.000Z&to=2026-07-02T00%3A00%3A00.000Z&baseAsset=ujuno"eAsset=ibc%2Fusdc&limit=25", undefined); + expect(candles.meta?.interval).toBe("1h"); + }); + + it("throws on unavailable indexer responses", async () => { + const fetcher = vi.fn(async () => new Response("nope", { status: 503 })) as unknown as typeof fetch; + const client = createIndexerClient({ baseUrl: "https://indexer.example", fetcher }); + await expect(client.health()).rejects.toThrow("Indexer request failed: 503"); + }); +}); diff --git a/frontend/src/lib/indexer/client.ts b/frontend/src/lib/indexer/client.ts new file mode 100644 index 000000000..fa54a5621 --- /dev/null +++ b/frontend/src/lib/indexer/client.ts @@ -0,0 +1,90 @@ +import type { IndexerCandleInterval, IndexerHealth, IndexerPage, IndexerPoolCandlesResponse, IndexerPoolMetrics, IndexerPoolPosition, IndexerPrice, IndexerProtocolStats, IndexerWalletTransaction } from "./types"; + +export type IndexerClientOptions = { + baseUrl: string; + fetcher?: typeof fetch; + timeoutMs?: number; +}; + +export class IndexerRequestError extends Error { + readonly status?: number; + readonly code: "disabled" | "http" | "timeout" | "network" | "invalid-response"; + + constructor(message: string, options: { status?: number; code: IndexerRequestError["code"]; cause?: unknown }) { + super(message); + this.name = "IndexerRequestError"; + this.status = options.status; + this.code = options.code; + this.cause = options.cause; + } +} + +function trimBaseUrl(baseUrl: string) { + return baseUrl.replace(/\/$/, ""); +} + +async function getJson(fetcher: typeof fetch, url: string, timeoutMs?: number): Promise { + const controller = timeoutMs ? new AbortController() : undefined; + const timeout = controller ? globalThis.setTimeout(() => controller.abort(), timeoutMs) : undefined; + try { + const response = await fetcher(url, controller ? { signal: controller.signal } : undefined); + if (!response.ok) throw new IndexerRequestError(`Indexer request failed: ${response.status}`, { status: response.status, code: "http" }); + return await response.json() as T; + } catch (error) { + if (error instanceof IndexerRequestError) throw error; + if (error instanceof DOMException && error.name === "AbortError") { + throw new IndexerRequestError("Indexer request timed out", { code: "timeout", cause: error }); + } + throw new IndexerRequestError("Indexer request failed", { code: "network", cause: error }); + } finally { + if (timeout) globalThis.clearTimeout(timeout); + } +} + +function withPagination(path: string, params: { limit?: number; cursor?: string } = {}) { + const query = new URLSearchParams(); + if (params.limit) query.set("limit", String(params.limit)); + if (params.cursor) query.set("cursor", params.cursor); + const suffix = query.toString(); + return suffix ? `${path}?${suffix}` : path; +} + +function candlesPath(id: string, params: { interval?: IndexerCandleInterval; from?: string; to?: string; baseAsset?: string; quoteAsset?: string; limit?: number; cursor?: string } = {}) { + const query = new URLSearchParams(); + if (params.interval) query.set("interval", params.interval); + if (params.from) query.set("from", params.from); + if (params.to) query.set("to", params.to); + if (params.baseAsset) query.set("baseAsset", params.baseAsset); + if (params.quoteAsset) query.set("quoteAsset", params.quoteAsset); + if (params.limit) query.set("limit", String(params.limit)); + if (params.cursor) query.set("cursor", params.cursor); + const suffix = query.toString(); + return `/pools/${encodeURIComponent(id)}/candles${suffix ? `?${suffix}` : ""}`; +} + +function pricesPath(assets: readonly string[]) { + const query = new URLSearchParams(); + query.set("assets", assets.join(",")); + return `/prices?${query.toString()}`; +} + +export function createIndexerClient({ baseUrl, fetcher = fetch, timeoutMs }: IndexerClientOptions) { + const root = trimBaseUrl(baseUrl); + return { + health: () => getJson(fetcher, `${root}/health`, timeoutMs), + stats: () => getJson(fetcher, `${root}/stats`, timeoutMs), + prices: (assets: readonly string[]) => getJson<{ data: IndexerPrice[] }>(fetcher, `${root}${pricesPath(assets)}`, timeoutMs), + price: (asset: string) => getJson(fetcher, `${root}/prices/${encodeURIComponent(asset)}`, timeoutMs), + pools: (params?: { limit?: number; cursor?: string }) => getJson>(fetcher, `${root}${withPagination("/pools", params)}`, timeoutMs), + pool: (id: string) => getJson(fetcher, `${root}/pools/${encodeURIComponent(id)}`, timeoutMs), + poolCandles: (id: string, params?: { interval?: IndexerCandleInterval; from?: string; to?: string; baseAsset?: string; quoteAsset?: string; limit?: number; cursor?: string }) => getJson(fetcher, `${root}${candlesPath(id, params)}`, timeoutMs), + poolPositions: (id: string, params?: { limit?: number; cursor?: string }) => getJson>(fetcher, `${root}${withPagination(`/pools/${encodeURIComponent(id)}/positions`, params)}`, timeoutMs), + poolHistory: (id: string, params?: { limit?: number; cursor?: string }) => getJson>(fetcher, `${root}${withPagination(`/pools/${encodeURIComponent(id)}/history`, params)}`, timeoutMs), + walletPositions: (address: string, params?: { limit?: number; cursor?: string }) => getJson>(fetcher, `${root}${withPagination(`/wallets/${encodeURIComponent(address)}/positions`, params)}`, timeoutMs), + walletHistory: (address: string, params?: { limit?: number; cursor?: string }) => getJson>(fetcher, `${root}${withPagination(`/wallets/${encodeURIComponent(address)}/history`, params)}`, timeoutMs), + }; +} + +export function getConfiguredIndexerBaseUrl() { + return (import.meta.env.VITE_DEX_INDEXER_URL as string | undefined)?.replace(/\/$/, ""); +} diff --git a/frontend/src/lib/indexer/types.ts b/frontend/src/lib/indexer/types.ts new file mode 100644 index 000000000..3f19f3504 --- /dev/null +++ b/frontend/src/lib/indexer/types.ts @@ -0,0 +1,152 @@ +export type IndexerPagination = { + limit: number; + nextCursor: string | null; +}; + +export type IndexerAssetAmount = { + denom: string; + symbol?: string; + reserve?: string | null; + amount?: string; + valueUsd?: number | null; + valueJuno?: number | null; + priceUsd?: number | null; + priceJuno?: number | null; + priceStatus?: "fresh" | "stale" | "missing" | string | null; + priceSource?: string | null; + priceUpdatedAt?: string | null; + isPriceMock?: boolean; +}; + +export type IndexerPrice = { + asset: string | null; + priceUsd: number | null; + priceJuno?: number | null; + source: string | null; + status: "fresh" | "stale" | "missing" | string; + stale: boolean; + observedAt: string | null; + ageMs: number | null; + isMock: boolean; +}; + +export type IndexerPoolMetrics = { + id: string; + pair: string; + pairAddress: string; + lpToken: string | null; + poolType: string | null; + assets: IndexerAssetAmount[]; + totalShare?: string | null; + tvlUsd: number | null; + tvlJuno?: number | null; + volume24hUsd: number | null; + volume24hJuno?: number | null; + volume7dUsd: number | null; + volume7dJuno?: number | null; + feeBps: number | null; + fees24hUsd: number | null; + fees24hJuno?: number | null; + feeApr: number; + incentivesApr: number; + totalApr: number; + incentivized: boolean; + updatedAt: string; + dataSource: "indexer" | "mock" | string; + isMock: boolean; +}; + +export type IndexerCandleInterval = "5m" | "1h" | "1d"; + +export type IndexerPoolCandle = { + poolId: string | null; + pairAddress: string | null; + baseAsset: string | null; + quoteAsset: string | null; + interval: IndexerCandleInterval | string; + bucketStart: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + volumeQuote: number; + tradeCount: number; + dataSource: "indexer" | "mock" | string; + isMock: boolean; +}; + +export type IndexerPoolCandlesResponse = IndexerPage & { + meta?: { + poolId?: string | null; + pairAddress?: string | null; + interval?: IndexerCandleInterval | string; + baseAsset?: string | null; + quoteAsset?: string | null; + from?: string | null; + to?: string | null; + dataSource?: "indexer" | "mock" | string; + isMock?: boolean; + }; +}; + +export type IndexerPoolPosition = { + walletAddress: string; + poolId: string; + pairAddress: string; + lpToken: string | null; + lpBalance: string; + bondedBalance?: string | null; + shareBps: number; + valueUsd: number | null; + valueJuno?: number | null; + assets: IndexerAssetAmount[]; + updatedAt: string; + dataSource: "indexer" | "mock" | string; + isMock: boolean; +}; + +export type IndexerWalletTransaction = { + txHash: string; + walletAddress: string | null; + poolId: string | null; + pairAddress: string | null; + type: "swap" | "provide_liquidity" | "withdraw_liquidity" | "claim_rewards" | string; + height: number; + timestamp: string; + offerAsset: IndexerAssetAmount | null; + askAsset: IndexerAssetAmount | null; + amountUsd: number | null; + feeUsd: number | null; + success: boolean; + dataSource: "indexer" | "mock" | string; + isMock: boolean; +}; + +export type IndexerProtocolStats = { + poolCount: number; + tvlUsd: number | null; + tvlJuno?: number | null; + volume24hUsd: number | null; + volume24hJuno?: number | null; + volume7dUsd: number | null; + volume7dJuno?: number | null; + fees24hUsd: number | null; + fees24hJuno?: number | null; + incentivizedPools: number; + updatedAt: string; + dataSource: "indexer" | "mock" | string; + isMock: boolean; +}; + +export type IndexerHealth = { + status: "ok" | string; + service: string; + dataSource: "indexer" | "mock" | string; + isMock: boolean; +}; + +export type IndexerPage = { + data: T[]; + pagination: IndexerPagination; +}; diff --git a/frontend/src/lib/liquidity/position.test.ts b/frontend/src/lib/liquidity/position.test.ts new file mode 100644 index 000000000..69ef67650 --- /dev/null +++ b/frontend/src/lib/liquidity/position.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import type { PoolResponse } from "../generated/Pair.types"; +import { calculateLpShareBps, estimateLpPosition, formatPositionSharePercent } from "./position"; + +const pool: PoolResponse = { + total_share: "1000000000", + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "5000000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "10000000000" }, + ], +}; + +describe("LP position math", () => { + it("calculates pool ownership in basis points", () => { + expect(calculateLpShareBps("50000000", pool.total_share)).toBe(500); + expect(calculateLpShareBps("1", "1000000000")).toBe(0); + expect(calculateLpShareBps("1000000000", pool.total_share)).toBe(10000); + expect(calculateLpShareBps(undefined, pool.total_share)).toBe(0); + expect(calculateLpShareBps("100", "0")).toBe(0); + }); + + it("estimates underlying position assets from wallet LP balance", () => { + expect(estimateLpPosition(pool, "50000000")).toMatchObject({ + lpBalance: "50000000", + totalShare: "1000000000", + shareBps: 500, + sharePercent: 5, + hasPosition: true, + underlyingAssets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "250000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "500000000" }, + ], + }); + }); + + it("formats LP share percentages for display", () => { + expect(formatPositionSharePercent(0)).toBe("0%"); + expect(formatPositionSharePercent(1)).toBe("0.01%"); + expect(formatPositionSharePercent(7)).toBe("0.07%"); + expect(formatPositionSharePercent(500)).toBe("5.00%"); + expect(formatPositionSharePercent(10000)).toBe("100.00%"); + }); +}); diff --git a/frontend/src/lib/liquidity/position.ts b/frontend/src/lib/liquidity/position.ts new file mode 100644 index 000000000..8e0670b8c --- /dev/null +++ b/frontend/src/lib/liquidity/position.ts @@ -0,0 +1,50 @@ +import type { Asset, PoolResponse } from "../generated/Pair.types"; +import { estimateWithdrawAssets } from "./withdraw"; + +const BPS_DENOMINATOR = 10_000n; + +function normalizeBaseAmount(amount: string | number | bigint | undefined): bigint { + if (amount === undefined || amount === null) return 0n; + const raw = String(amount).trim(); + if (!/^\d+$/.test(raw)) return 0n; + return BigInt(raw); +} + +export type LpPosition = { + lpBalance: string; + totalShare: string; + shareBps: number; + sharePercent: number; + underlyingAssets: Asset[]; + hasPosition: boolean; +}; + +export function calculateLpShareBps(lpBalance: string | undefined, totalShare: string | undefined): number { + const balance = normalizeBaseAmount(lpBalance); + const total = normalizeBaseAmount(totalShare); + if (balance <= 0n || total <= 0n) return 0; + const bps = (balance * BPS_DENOMINATOR) / total; + return Number(bps > BPS_DENOMINATOR ? BPS_DENOMINATOR : bps); +} + +export function formatPositionSharePercent(shareBps: number): string { + if (!Number.isFinite(shareBps) || shareBps <= 0) return "0%"; + if (shareBps < 1) return "<0.01%"; + const whole = Math.floor(shareBps / 100); + const fraction = shareBps % 100; + return `${whole}.${fraction.toString().padStart(2, "0")}%`; +} + +export function estimateLpPosition(pool: PoolResponse | undefined, lpBalance: string | undefined): LpPosition { + const normalizedBalance = normalizeBaseAmount(lpBalance).toString(); + const totalShare = normalizeBaseAmount(pool?.total_share).toString(); + const shareBps = calculateLpShareBps(normalizedBalance, totalShare); + return { + lpBalance: normalizedBalance, + totalShare, + shareBps, + sharePercent: shareBps / 100, + underlyingAssets: estimateWithdrawAssets(pool, normalizedBalance), + hasPosition: normalizeBaseAmount(normalizedBalance) > 0n, + }; +} diff --git a/frontend/src/lib/liquidity/provide.test.ts b/frontend/src/lib/liquidity/provide.test.ts new file mode 100644 index 000000000..fa314e3f2 --- /dev/null +++ b/frontend/src/lib/liquidity/provide.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { calculateInitialLiquidityQuote, calculateProvideLiquidityQuote, displayBaseAmount, formatLpShareBps, isEmptyPoolState, ratioAmount } from "./provide"; + +describe("provide liquidity math", () => { + it("balances the opposite side to the pool ratio", () => { + expect(ratioAmount("100", "1000", "2000")).toBe("200"); + expect(displayBaseAmount("1234567", 6)).toBe("1.234567"); + }); + + it("estimates LP mint and resulting pool share for proportional deposits", () => { + const quote = calculateProvideLiquidityQuote({ + depositAmounts: ["100", "200"], + reserves: ["1000", "2000"], + totalShare: "500", + }); + + expect(quote).toEqual({ + expectedLpAmount: "50", + poolShareBps: 909, + imbalanceBps: 0, + isProportional: true, + }); + expect(formatLpShareBps(quote?.poolShareBps ?? 0)).toBe("9.09%"); + }); + + it("flags non-proportional deposits", () => { + const quote = calculateProvideLiquidityQuote({ + depositAmounts: ["100", "100"], + reserves: ["1000", "2000"], + totalShare: "500", + }); + + expect(quote?.expectedLpAmount).toBe("25"); + expect(quote?.imbalanceBps).toBe(5000); + expect(quote?.isProportional).toBe(false); + }); + + it("does not quote empty or uninitialized pools", () => { + expect(calculateProvideLiquidityQuote({ depositAmounts: ["0", "1"], reserves: ["10", "10"], totalShare: "10" })).toBeNull(); + expect(calculateProvideLiquidityQuote({ depositAmounts: ["1", "1"], reserves: ["0", "10"], totalShare: "10" })).toBeNull(); + }); + + it("detects empty first-provider pools and previews the initial price", () => { + expect(isEmptyPoolState(["0", "0"], "0")).toBe(true); + expect(isEmptyPoolState(["100", "200"], "50")).toBe(false); + + expect(calculateInitialLiquidityQuote({ + depositAmounts: ["100000", "500000"], + decimals: [6, 6], + reserves: ["0", "0"], + totalShare: "0", + })).toEqual({ + isFirstProvider: true, + price0In1: "5", + price1In0: "0.2", + }); + }); +}); diff --git a/frontend/src/lib/liquidity/provide.ts b/frontend/src/lib/liquidity/provide.ts new file mode 100644 index 000000000..ec446b7f8 --- /dev/null +++ b/frontend/src/lib/liquidity/provide.ts @@ -0,0 +1,116 @@ +import { formatAmount } from "../format/amounts"; + +const BPS_DENOMINATOR = 10_000n; + +export type ProvideLiquidityQuote = { + expectedLpAmount: string; + poolShareBps: number; + imbalanceBps: number; + isProportional: boolean; +}; + +export type InitialLiquidityQuote = { + isFirstProvider: boolean; + price0In1: string | null; + price1In0: string | null; +}; + +function normalizeBaseAmount(amount: string | number | bigint | undefined): bigint { + if (amount === undefined || amount === null) return 0n; + const raw = String(amount).trim(); + if (!/^\d+$/.test(raw)) return 0n; + return BigInt(raw); +} + +export function ratioAmount(inputAmount: string, inputReserve: string, outputReserve: string): string { + const amount = normalizeBaseAmount(inputAmount); + const reserveIn = normalizeBaseAmount(inputReserve); + const reserveOut = normalizeBaseAmount(outputReserve); + if (amount <= 0n || reserveIn <= 0n || reserveOut <= 0n) return "0"; + return ((amount * reserveOut) / reserveIn).toString(); +} + +export function calculateProvideLiquidityQuote({ + depositAmounts, + reserves, + totalShare, +}: { + depositAmounts: [string, string]; + reserves: [string, string]; + totalShare: string; +}): ProvideLiquidityQuote | null { + const amount0 = normalizeBaseAmount(depositAmounts[0]); + const amount1 = normalizeBaseAmount(depositAmounts[1]); + const reserve0 = normalizeBaseAmount(reserves[0]); + const reserve1 = normalizeBaseAmount(reserves[1]); + const share = normalizeBaseAmount(totalShare); + + if (amount0 <= 0n || amount1 <= 0n || reserve0 <= 0n || reserve1 <= 0n || share <= 0n) return null; + + const lpFrom0 = (amount0 * share) / reserve0; + const lpFrom1 = (amount1 * share) / reserve1; + const expectedLpAmount = lpFrom0 < lpFrom1 ? lpFrom0 : lpFrom1; + const denominator = share + expectedLpAmount; + const poolShareBps = denominator > 0n ? Number((expectedLpAmount * BPS_DENOMINATOR) / denominator) : 0; + + const ideal1 = (amount0 * reserve1) / reserve0; + const diff = amount1 > ideal1 ? amount1 - ideal1 : ideal1 - amount1; + const imbalanceBps = ideal1 > 0n ? Number((diff * BPS_DENOMINATOR) / ideal1) : 0; + + return { + expectedLpAmount: expectedLpAmount.toString(), + poolShareBps, + imbalanceBps, + isProportional: imbalanceBps <= 1, + }; +} + +export function isEmptyPoolState(reserves: [string, string] | undefined, totalShare: string | undefined): boolean { + if (!reserves) return false; + const reserve0 = normalizeBaseAmount(reserves[0]); + const reserve1 = normalizeBaseAmount(reserves[1]); + const share = normalizeBaseAmount(totalShare); + return share <= 0n || (reserve0 <= 0n && reserve1 <= 0n); +} + +function decimalRatio(numerator: bigint, denominator: bigint, precision = 8): string | null { + if (numerator <= 0n || denominator <= 0n) return null; + const scale = 10n ** BigInt(precision); + const scaled = (numerator * scale) / denominator; + const whole = scaled / scale; + const fraction = (scaled % scale).toString().padStart(precision, "0").replace(/0+$/, ""); + return fraction ? `${whole}.${fraction}` : whole.toString(); +} + +export function calculateInitialLiquidityQuote({ + depositAmounts, + decimals, + reserves, + totalShare, +}: { + depositAmounts: [string, string]; + decimals: [number, number]; + reserves: [string, string] | undefined; + totalShare: string | undefined; +}): InitialLiquidityQuote { + const amount0 = normalizeBaseAmount(depositAmounts[0]); + const amount1 = normalizeBaseAmount(depositAmounts[1]); + const decimalAdjusted0 = amount0 * 10n ** BigInt(decimals[1]); + const decimalAdjusted1 = amount1 * 10n ** BigInt(decimals[0]); + return { + isFirstProvider: isEmptyPoolState(reserves, totalShare), + price0In1: decimalRatio(decimalAdjusted1, decimalAdjusted0), + price1In0: decimalRatio(decimalAdjusted0, decimalAdjusted1), + }; +} + +export function formatLpShareBps(bps: number): string { + const percent = bps / 100; + if (percent === 0) return "0%"; + if (percent < 0.01) return "<0.01%"; + return `${percent.toFixed(percent >= 10 ? 2 : 4).replace(/0+$/, "").replace(/\.$/, "")}%`; +} + +export function displayBaseAmount(baseAmount: string, decimals: number): string { + return formatAmount(baseAmount, decimals, decimals).replace(/,/g, ""); +} diff --git a/frontend/src/lib/liquidity/withdraw.test.ts b/frontend/src/lib/liquidity/withdraw.test.ts new file mode 100644 index 000000000..b6c5a89c3 --- /dev/null +++ b/frontend/src/lib/liquidity/withdraw.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import type { PoolResponse } from "../generated/Pair.types"; +import { applySlippageToAssets, calculatePercentageFill, estimateWithdrawAssets } from "./withdraw"; + +const pool: PoolResponse = { + total_share: "1000000000", + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "5000000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "10000000000" }, + ], +}; + +describe("withdraw liquidity math", () => { + it("calculates percentage fills from LP balances", () => { + expect(calculatePercentageFill("100000000", 25)).toBe("25000000"); + expect(calculatePercentageFill("100000000", 50)).toBe("50000000"); + expect(calculatePercentageFill("3", 50)).toBe("1"); + expect(calculatePercentageFill(undefined, 100)).toBe("0"); + }); + + it("estimates proportional underlying assets from reserves and total share", () => { + expect(estimateWithdrawAssets(pool, "50000000")).toEqual([ + { info: { native_token: { denom: "ujuno" } }, amount: "250000000" }, + { info: { native_token: { denom: "factory/pair/token" } }, amount: "500000000" }, + ]); + }); + + it("computes minimum received assets after slippage", () => { + const expected = estimateWithdrawAssets(pool, "50000000"); + expect(applySlippageToAssets(expected, 50).map((asset) => asset.amount)).toEqual(["248750000", "497500000"]); + }); +}); diff --git a/frontend/src/lib/liquidity/withdraw.ts b/frontend/src/lib/liquidity/withdraw.ts new file mode 100644 index 000000000..98e9ced1d --- /dev/null +++ b/frontend/src/lib/liquidity/withdraw.ts @@ -0,0 +1,36 @@ +import type { Asset, PoolResponse } from "../generated/Pair.types"; + +const BPS_DENOMINATOR = 10_000n; + +function normalizeBaseAmount(amount: string | number | bigint | undefined): bigint { + if (amount === undefined || amount === null) return 0n; + const raw = String(amount).trim(); + if (!/^\d+$/.test(raw)) return 0n; + return BigInt(raw); +} + +export function calculatePercentageFill(balanceBaseAmount: string | undefined, percent: number): string { + const balance = normalizeBaseAmount(balanceBaseAmount); + if (balance <= 0n || !Number.isFinite(percent) || percent <= 0) return "0"; + const boundedPercent = Math.min(100, Math.max(0, Math.round(percent))); + return ((balance * BigInt(boundedPercent)) / 100n).toString(); +} + +export function estimateWithdrawAssets(pool: PoolResponse | undefined, lpAmount: string): Asset[] { + const share = normalizeBaseAmount(lpAmount); + const totalShare = normalizeBaseAmount(pool?.total_share); + if (!pool || share <= 0n || totalShare <= 0n) return []; + + return pool.assets.map((asset) => ({ + info: asset.info, + amount: ((normalizeBaseAmount(asset.amount) * share) / totalShare).toString(), + })); +} + +export function applySlippageToAssets(assets: readonly Asset[], slippageBps: number): Asset[] { + const safeBps = Number.isFinite(slippageBps) ? Math.min(10_000, Math.max(0, Math.round(slippageBps))) : 0; + return assets.map((asset) => ({ + info: asset.info, + amount: ((normalizeBaseAmount(asset.amount) * (BPS_DENOMINATOR - BigInt(safeBps))) / BPS_DENOMINATOR).toString(), + })); +} diff --git a/frontend/src/lib/pools/poolList.test.ts b/frontend/src/lib/pools/poolList.test.ts new file mode 100644 index 000000000..52db0a83b --- /dev/null +++ b/frontend/src/lib/pools/poolList.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { DEFAULT_POOL_LIST_CONTROLS, filterAndSortPools } from "./poolList"; + +function pool(overrides: Partial & Pick): RegistryPool { + return { + lpToken: `${overrides.id}-lp`, + assets: [ + { kind: "native", id: `${overrides.id}-base`, symbol: overrides.label.split(" /")[0] ?? "AAA", decimals: 6 }, + { kind: "native", id: `${overrides.id}-quote`, symbol: overrides.label.split(" /")[1] ?? "BBB", decimals: 6 }, + ], + explorer: `https://example.com/${overrides.pair}`, + enabled: true, + verified: true, + ...overrides, + status: overrides.status ?? "active", + }; +} + +const pools = [ + pool({ id: "juno-usdc", label: "JUNO / USDC", pair: "juno1alpha", type: "xyk", feeBps: 30, featured: true }), + pool({ id: "atom-usdc", label: "ATOM / USDC", pair: "juno1beta", type: "stable", feeBps: 5, verified: false }), + pool({ id: "raw-wynd", label: "RAW / WYND", pair: "juno1gamma", type: "xyk", feeBps: 30 }), +]; + +describe("pool list filtering and sorting", () => { + it("searches labels, symbols, denoms, and addresses", () => { + expect(filterAndSortPools(pools, { ...DEFAULT_POOL_LIST_CONTROLS, search: "atom" }).map((candidate) => candidate.id)).toEqual(["atom-usdc"]); + expect(filterAndSortPools(pools, { ...DEFAULT_POOL_LIST_CONTROLS, search: "juno1gamma" }).map((candidate) => candidate.id)).toEqual(["raw-wynd"]); + }); + + it("filters by type and verification", () => { + expect(filterAndSortPools(pools, { ...DEFAULT_POOL_LIST_CONTROLS, type: "stable" }).map((candidate) => candidate.id)).toEqual(["atom-usdc"]); + expect(filterAndSortPools(pools, { ...DEFAULT_POOL_LIST_CONTROLS, verified: "unverified" }).map((candidate) => candidate.id)).toEqual(["atom-usdc"]); + }); + + it("sorts numeric metrics with unavailable values last", () => { + const sorted = filterAndSortPools(pools, { ...DEFAULT_POOL_LIST_CONTROLS, sortKey: "tvl", sortDirection: "desc" }, { + juno1alpha: { tvlUsd: 1200 }, + juno1beta: { tvlUsd: 9900 }, + }); + + expect(sorted.map((candidate) => candidate.id)).toEqual(["atom-usdc", "juno-usdc", "raw-wynd"]); + }); + + it("filters incentivized pools from indexer metrics", () => { + const sorted = filterAndSortPools(pools, { ...DEFAULT_POOL_LIST_CONTROLS, incentivized: "incentivized" }, { + juno1gamma: { incentivesApr: 8.4, incentivized: true }, + }); + + expect(sorted.map((candidate) => candidate.id)).toEqual(["raw-wynd"]); + }); +}); diff --git a/frontend/src/lib/pools/poolList.ts b/frontend/src/lib/pools/poolList.ts new file mode 100644 index 000000000..cbd2f45ac --- /dev/null +++ b/frontend/src/lib/pools/poolList.ts @@ -0,0 +1,124 @@ +import type { RegistryPool } from "../../config/registry"; + +export type PoolMetricValue = number | null | undefined; + +export type PoolMetrics = { + tvlUsd?: PoolMetricValue; + tvlJuno?: PoolMetricValue; + volume24hUsd?: PoolMetricValue; + volume24hJuno?: PoolMetricValue; + feeApr?: PoolMetricValue; + incentivesApr?: PoolMetricValue; + totalApr?: PoolMetricValue; + incentivized?: boolean; + source?: "indexer" | "mock" | "fallback" | "disabled"; + isMock?: boolean; + isStale?: boolean; + updatedAt?: string; +}; + +export type PoolListSortKey = "featured" | "pool" | "tvl" | "volume" | "apr"; +export type PoolListSortDirection = "asc" | "desc"; +export type PoolTypeFilter = "all" | RegistryPool["type"]; +export type PoolVerifiedFilter = "all" | "verified" | "unverified"; +export type PoolIncentiveFilter = "all" | "incentivized" | "unincentivized"; + +export type PoolListControls = { + search: string; + type: PoolTypeFilter; + verified: PoolVerifiedFilter; + incentivized: PoolIncentiveFilter; + sortKey: PoolListSortKey; + sortDirection: PoolListSortDirection; +}; + +export type PoolMetricsByPair = Record; + +export const DEFAULT_POOL_LIST_CONTROLS: PoolListControls = { + search: "", + type: "all", + verified: "all", + incentivized: "all", + sortKey: "featured", + sortDirection: "desc", +}; + +function normalizeSearch(value: string) { + return value.trim().toLowerCase(); +} + +export function poolMatchesSearch(pool: RegistryPool, search: string) { + const query = normalizeSearch(search); + if (!query) return true; + + const haystack = [ + pool.label, + pool.id, + pool.pair, + pool.lpToken, + pool.type, + ...pool.assets.flatMap((asset) => [asset.symbol, asset.id, asset.denomTrace ?? ""]), + ].join(" ").toLowerCase(); + + return haystack.includes(query); +} + +function metricNumber(pool: RegistryPool, metricsByPair: PoolMetricsByPair, sortKey: PoolListSortKey) { + const metrics = metricsByPair[pool.pair]; + if (sortKey === "tvl") return metrics?.tvlUsd ?? metrics?.tvlJuno; + if (sortKey === "volume") return metrics?.volume24hUsd ?? metrics?.volume24hJuno; + if (sortKey === "apr") return metrics?.totalApr ?? metrics?.feeApr; + return undefined; +} + +function compareMetricValues(a: PoolMetricValue, b: PoolMetricValue, direction: PoolListSortDirection) { + const aNumber = typeof a === "number" && Number.isFinite(a) ? a : undefined; + const bNumber = typeof b === "number" && Number.isFinite(b) ? b : undefined; + if (aNumber === undefined && bNumber === undefined) return 0; + if (aNumber === undefined) return 1; + if (bNumber === undefined) return -1; + return direction === "asc" ? aNumber - bNumber : bNumber - aNumber; +} + +export function getPoolTotalApr(metrics: PoolMetrics | undefined) { + if (!metrics) return undefined; + if (typeof metrics.totalApr === "number") return metrics.totalApr; + const parts = [metrics.feeApr, metrics.incentivesApr].filter((value): value is number => typeof value === "number" && Number.isFinite(value)); + if (parts.length === 0) return undefined; + return parts.reduce((sum, value) => sum + value, 0); +} + +export function filterAndSortPools( + pools: RegistryPool[], + controls: PoolListControls, + metricsByPair: PoolMetricsByPair = {}, +) { + const filtered = pools.filter((pool) => { + if (!poolMatchesSearch(pool, controls.search)) return false; + if (controls.type !== "all" && pool.type !== controls.type) return false; + if (controls.verified === "verified" && pool.verified === false) return false; + if (controls.verified === "unverified" && pool.verified !== false) return false; + + const isIncentivized = Boolean(metricsByPair[pool.pair]?.incentivized || (metricsByPair[pool.pair]?.incentivesApr ?? 0) > 0); + if (controls.incentivized === "incentivized" && !isIncentivized) return false; + if (controls.incentivized === "unincentivized" && isIncentivized) return false; + + return true; + }); + + return [...filtered].sort((a, b) => { + if (controls.sortKey === "featured") { + return Number(Boolean(b.featured)) - Number(Boolean(a.featured)) || a.label.localeCompare(b.label); + } + if (controls.sortKey === "pool") { + return controls.sortDirection === "asc" ? a.label.localeCompare(b.label) : b.label.localeCompare(a.label); + } + + const metricComparison = compareMetricValues( + metricNumber(a, metricsByPair, controls.sortKey), + metricNumber(b, metricsByPair, controls.sortKey), + controls.sortDirection, + ); + return metricComparison || a.label.localeCompare(b.label); + }); +} diff --git a/frontend/src/lib/pools/poolTypes.test.ts b/frontend/src/lib/pools/poolTypes.test.ts new file mode 100644 index 000000000..716ccd93a --- /dev/null +++ b/frontend/src/lib/pools/poolTypes.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import { getPoolTypeLabel, getPoolTypeMetadata, hasCaveatedLocalMath } from "./poolTypes"; + +const basePool: RegistryPool = { + id: "pool", + label: "AAA / BBB", + pair: "juno1pair", + lpToken: "factory/juno1pair/lp", + type: "xyk", + feeBps: 30, + assets: [ + { kind: "native", id: "uaaa", symbol: "AAA", decimals: 6 }, + { kind: "native", id: "ubbb", symbol: "BBB", decimals: 6 }, + ], + explorer: "https://example.com/pair", + enabled: true, + status: "active", +}; + +describe("pool type metadata", () => { + it("classifies XYK as locally supported for proportional liquidity math", () => { + const metadata = getPoolTypeMetadata("xyk"); + expect(metadata.shortLabel).toBe("XYK"); + expect(metadata.supportsLocalPriceImpact).toBe(true); + expect(metadata.supportsProvideLiquidity).toBe(true); + expect(hasCaveatedLocalMath(basePool)).toBe(false); + }); + + it("classifies stableswap and PCL as contract-simulated with caveated local liquidity math", () => { + const stable = getPoolTypeMetadata("stable"); + const concentrated = getPoolTypeMetadata("concentrated"); + + expect(getPoolTypeLabel("stable")).toBe("Stable"); + expect(stable.supportsSwapSimulation).toBe(true); + expect(stable.supportsProvideLiquidity).toBe(false); + expect(stable.supportsLocalPriceImpact).toBe(false); + expect(concentrated.shortLabel).toBe("PCL"); + expect(concentrated.supportsProvideSimulation).toBe(false); + expect(hasCaveatedLocalMath({ ...basePool, type: "stable" })).toBe(true); + expect(hasCaveatedLocalMath({ ...basePool, type: "concentrated" })).toBe(true); + }); +}); diff --git a/frontend/src/lib/pools/poolTypes.ts b/frontend/src/lib/pools/poolTypes.ts new file mode 100644 index 000000000..a43f341e3 --- /dev/null +++ b/frontend/src/lib/pools/poolTypes.ts @@ -0,0 +1,92 @@ +import type { RegistryPool } from "../../config/registry"; + +export type PoolType = RegistryPool["type"]; + +export type PoolTypeMetadata = { + label: string; + shortLabel: string; + badgeClass: "status-ok" | "status-warn" | "status-danger"; + description: string; + swapCopy: string; + feeCopy: string; + provideCopy: string; + withdrawCopy: string; + detailCopy: string; + createCopy: string; + supportsSwapSimulation: boolean; + supportsExactOutSimulation: boolean; + supportsLocalPriceImpact: boolean; + supportsProvideLiquidity: boolean; + supportsProvideSimulation: boolean; + supportsWithdrawSimulation: boolean; +}; + +const POOL_TYPE_METADATA: Record = { + xyk: { + label: "XYK constant product", + shortLabel: "XYK", + badgeClass: "status-ok", + description: "Standard x*y=k pair for volatile assets.", + swapCopy: "Direct pair simulation returns contract pricing, spread, and fee for this XYK pool.", + feeCopy: "Constant-product fee tier", + provideCopy: "Two-sided deposits are balanced to the live XYK reserve ratio. Single-sided add liquidity is not enabled.", + withdrawCopy: "Withdraw estimates burn LP shares for the two pool assets proportionally.", + detailCopy: "Volatile-pair pricing uses the constant product invariant; local provide estimates are enabled for proportional two-sided deposits.", + createCopy: "XYK creation will use the factory xyk pair type once create transactions are enabled.", + supportsSwapSimulation: true, + supportsExactOutSimulation: true, + supportsLocalPriceImpact: true, + supportsProvideLiquidity: true, + supportsProvideSimulation: true, + supportsWithdrawSimulation: true, + }, + stable: { + label: "Stableswap", + shortLabel: "Stable", + badgeClass: "status-warn", + description: "Stable invariant pair for closely-pegged assets.", + swapCopy: "Swaps use on-chain stable pair simulation. The UI does not reimplement the stable invariant locally.", + feeCopy: "Stable fee tier", + provideCopy: "Stable provide math depends on amplification and pool parameters. The UI does not simulate stable deposits yet, so add liquidity is disabled here.", + withdrawCopy: "Withdrawal output is shown as a proportional LP estimate only; confirm final assets in the wallet before signing.", + detailCopy: "Stable pool parameters are contract-defined. Treat local price impact and liquidity estimates as caveated unless returned directly by pair/router simulation.", + createCopy: "Stable creation needs amplification and stable-pair params; the transaction builder is not exposed yet.", + supportsSwapSimulation: true, + supportsExactOutSimulation: true, + supportsLocalPriceImpact: false, + supportsProvideLiquidity: false, + supportsProvideSimulation: false, + supportsWithdrawSimulation: false, + }, + concentrated: { + label: "PCL concentrated liquidity", + shortLabel: "PCL", + badgeClass: "status-warn", + description: "Passive concentrated liquidity pair with PCL-specific parameters.", + swapCopy: "Swaps use on-chain PCL pair simulation. The UI does not reimplement PCL math locally.", + feeCopy: "PCL fee tier", + provideCopy: "PCL provide rules depend on concentration parameters and live contract math. Add liquidity is disabled until PCL provide simulation is wired.", + withdrawCopy: "Withdrawal output is shown as a proportional LP estimate only; confirm final assets in the wallet before signing.", + detailCopy: "PCL pools have concentration parameters not modeled locally. Use contract quotes and verify slippage carefully.", + createCopy: "PCL creation needs concentration parameters; the transaction builder is not exposed yet.", + supportsSwapSimulation: true, + supportsExactOutSimulation: true, + supportsLocalPriceImpact: false, + supportsProvideLiquidity: false, + supportsProvideSimulation: false, + supportsWithdrawSimulation: false, + }, +}; + +export function getPoolTypeMetadata(type: PoolType): PoolTypeMetadata { + return POOL_TYPE_METADATA[type]; +} + +export function getPoolTypeLabel(type: PoolType): string { + return getPoolTypeMetadata(type).shortLabel; +} + +export function hasCaveatedLocalMath(pool: RegistryPool): boolean { + const metadata = getPoolTypeMetadata(pool.type); + return !metadata.supportsLocalPriceImpact || !metadata.supportsProvideSimulation || !metadata.supportsWithdrawSimulation; +} diff --git a/frontend/src/lib/portfolio/portfolio.test.ts b/frontend/src/lib/portfolio/portfolio.test.ts new file mode 100644 index 000000000..06d7ace09 --- /dev/null +++ b/frontend/src/lib/portfolio/portfolio.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryPool } from "../../config/registry"; +import type { PoolResponse } from "../generated/Pair.types"; +import { buildPortfolioSummary, totalLpBalance } from "./portfolio"; + +const pool: RegistryPool = { + id: "juno-usdc", + label: "JUNO / USDC", + pair: "juno1pair00000000000000000000000000000000000000", + lpToken: "factory/juno1pair/astroport/share", + type: "xyk", + feeBps: 30, + explorer: "https://ping.pub/juno", + enabled: true, + status: "active", + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6 }, + { kind: "native", id: "ibc/usdc", symbol: "USDC", decimals: 6 }, + ], +}; + +const reserve: PoolResponse = { + total_share: "1000000000", + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "5000000000" }, + { info: { native_token: { denom: "ibc/usdc" } }, amount: "10000000000" }, + ], +}; + +const balances = [ + { denom: pool.lpToken, symbol: "JUNO/USDC LP", decimals: 6, source: "lp" as const, amount: "100000000", isKnownDenom: true }, + { denom: "ujuno", symbol: "JUNO", decimals: 6, source: "registry" as const, amount: "2000000", isKnownDenom: true }, +]; + +describe("portfolio summary", () => { + it("returns an empty disconnected wallet summary without positions", () => { + const summary = buildPortfolioSummary({ pools: [pool] }); + + expect(summary.positions).toEqual([]); + expect(summary.totalLpValueUsd).toBeNull(); + expect(summary.walletBalances).toEqual([]); + }); + + it("prefers indexer positions including staked LP and claimable rewards", () => { + const summary = buildPortfolioSummary({ + pools: [pool], + balances, + reservesByPair: { [pool.pair]: reserve }, + preferIndexer: true, + indexerPositions: [{ + walletAddress: "juno1wallet", + poolId: pool.id, + pairAddress: pool.pair, + lpToken: pool.lpToken, + lpBalance: "100000000", + stakedLpBalance: "50000000", + shareBps: 1000, + valueUsd: 42.5, + valueJuno: 100, + updatedAt: "2026-07-02T00:00:00.000Z", + dataSource: "indexer", + isMock: false, + assets: [ + { denom: "ujuno", symbol: "JUNO", amount: "500000000", valueUsd: 20, valueJuno: 50, priceStatus: "fresh" }, + { denom: "ibc/usdc", symbol: "USDC", amount: "1000000000", valueUsd: 22.5, valueJuno: 50, priceStatus: "fresh" }, + ], + claimableRewards: [{ denom: "ujuno", symbol: "JUNO", amount: "1000000", valueUsd: 0.75, valueJuno: 1, priceStatus: "fresh" }], + }], + }); + + expect(summary.positions).toHaveLength(1); + expect(summary.positions[0]).toMatchObject({ source: "indexer", stakedLpBalance: "50000000", valueUsd: 42.5 }); + expect(totalLpBalance(summary.positions[0])).toBe("150000000"); + expect(summary.totalLpValueUsd).toBe(42.5); + expect(summary.totalLpValueJuno).toBe(100); + expect(summary.totalClaimableUsd).toBe(0.75); + expect(summary.totalClaimableJuno).toBe(1); + }); + + it("uses Juno values when USD prices are missing", () => { + const summary = buildPortfolioSummary({ + pools: [pool], + preferIndexer: true, + indexerPositions: [{ + walletAddress: "juno1wallet", + poolId: pool.id, + pairAddress: pool.pair, + lpToken: pool.lpToken, + lpBalance: "100000000", + bondedBalance: "50000000", + shareBps: 1000, + valueUsd: null, + valueJuno: 125, + updatedAt: "2026-07-02T00:00:00.000Z", + dataSource: "indexer", + isMock: false, + assets: [ + { denom: "ujuno", symbol: "JUNO", amount: "500000000", valueUsd: null, valueJuno: 100, priceStatus: "missing" }, + ], + claimableRewards: [{ denom: "ujuno", symbol: "JUNO", amount: "1000000", valueUsd: null, valueJuno: 1, priceStatus: "missing" }], + }], + }); + + expect(summary.positions[0]).toMatchObject({ stakedLpBalance: "50000000", valueUsd: null, valueJuno: 125 }); + expect(totalLpBalance(summary.positions[0])).toBe("150000000"); + expect(summary.totalLpValueUsd).toBeNull(); + expect(summary.totalLpValueJuno).toBe(125); + expect(summary.totalClaimableUsd).toBeNull(); + expect(summary.totalClaimableJuno).toBe(1); + expect(summary.missingPositionPrices).toBe(0); + expect(summary.missingRewardPrices).toBe(0); + }); + + it("adds pending incentives rewards when indexer positions do not include rewards", () => { + const summary = buildPortfolioSummary({ + pools: [pool], + preferIndexer: true, + incentivesByLpToken: { + [pool.lpToken]: { + configured: true, + lpToken: pool.lpToken, + stakedAmount: "50000000", + pendingRewards: [{ info: { native_token: { denom: "ujuno" } }, amount: "2500000" }], + rewardInfo: [], + }, + }, + indexerPositions: [{ + walletAddress: "juno1wallet", + poolId: pool.id, + pairAddress: pool.pair, + lpToken: pool.lpToken, + lpBalance: "100000000", + shareBps: 1000, + valueUsd: null, + valueJuno: 125, + updatedAt: "2026-07-02T00:00:00.000Z", + dataSource: "indexer", + isMock: false, + assets: [], + }], + }); + + expect(summary.positions[0].stakedLpBalance).toBe("50000000"); + expect(summary.positions[0].rewards[0]).toMatchObject({ denom: "ujuno", symbol: "JUNO", amount: "2500000", valueJuno: 2.5 }); + expect(summary.claimableRewardCount).toBe(1); + expect(summary.totalClaimableUsd).toBeNull(); + expect(summary.totalClaimableJuno).toBe(2.5); + expect(summary.missingRewardPrices).toBe(0); + }); + + it("falls back to on-chain LP balances and reserves when the indexer is unavailable", () => { + const summary = buildPortfolioSummary({ + pools: [pool], + balances, + reservesByPair: { [pool.pair]: reserve }, + preferIndexer: false, + indexerPositions: [], + }); + + expect(summary.positions).toHaveLength(1); + expect(summary.positions[0]).toMatchObject({ source: "on-chain", lpBalance: "100000000", shareBps: 1000, valueUsd: null }); + expect(summary.positions[0].assets.map((asset) => asset.amount)).toEqual(["500000000", "1000000000"]); + }); + + it("keeps aggregate value unknown when any position price is missing", () => { + const summary = buildPortfolioSummary({ + pools: [pool], + preferIndexer: true, + indexerPositions: [{ + walletAddress: "juno1wallet", + poolId: pool.id, + pairAddress: pool.pair, + lpToken: pool.lpToken, + lpBalance: "100000000", + shareBps: 1000, + valueUsd: null, + updatedAt: "2026-07-02T00:00:00.000Z", + dataSource: "indexer", + isMock: false, + assets: [{ denom: "ujuno", symbol: "JUNO", amount: "500000000", valueUsd: null, priceStatus: "missing" }], + }], + }); + + expect(summary.totalLpValueUsd).toBeNull(); + expect(summary.missingPositionPrices).toBe(1); + }); +}); diff --git a/frontend/src/lib/portfolio/portfolio.ts b/frontend/src/lib/portfolio/portfolio.ts new file mode 100644 index 000000000..db0acb9de --- /dev/null +++ b/frontend/src/lib/portfolio/portfolio.ts @@ -0,0 +1,229 @@ +import type { RegistryPool } from "../../config/registry"; +import type { PoolResponse } from "../generated/Pair.types"; +import type { Asset } from "../generated/Incentives.types"; +import type { IncentivesPoolState } from "../incentives"; +import type { IndexerAssetAmount, IndexerPoolPosition } from "../indexer/types"; +import { estimateLpPosition } from "../liquidity/position"; +import type { WalletBalance } from "../../queries/useWalletBalances"; + +export type PortfolioAssetAmount = { + denom: string; + symbol: string; + amount: string; + decimals: number; + valueUsd: number | null; + valueJuno: number | null; + priceStatus: "fresh" | "stale" | "missing" | "unknown" | string; + priceSource?: string | null; +}; + +export type PortfolioReward = { + denom: string; + symbol: string; + amount: string; + valueUsd: number | null; + valueJuno: number | null; + status: "claimable" | "unavailable"; +}; + +export type PortfolioPosition = { + id: string; + pool: RegistryPool; + source: "indexer" | "mock" | "on-chain"; + isStale: boolean; + lpBalance: string; + stakedLpBalance: string | null; + shareBps: number; + valueUsd: number | null; + valueJuno: number | null; + assets: PortfolioAssetAmount[]; + rewards: PortfolioReward[]; +}; + +export type PortfolioSummary = { + positions: PortfolioPosition[]; + walletBalances: WalletBalance[]; + totalLpValueUsd: number | null; + totalLpValueJuno: number | null; + totalClaimableUsd: number | null; + totalClaimableJuno: number | null; + missingPositionPrices: number; + missingRewardPrices: number; + claimableRewardCount: number; +}; + +type IndexerPositionWithOptionalRewards = IndexerPoolPosition & { + stakedLpBalance?: string | null; + staked_balance?: string | null; + bondedBalance?: string | null; + bonded_balance?: string | null; + rewards?: IndexerAssetAmount[]; + claimableRewards?: IndexerAssetAmount[]; + claimable_rewards?: IndexerAssetAmount[]; +}; + +function baseAmount(value: string | undefined | null) { + if (!value || !/^\d+$/.test(value)) return 0n; + return BigInt(value); +} + +function sumBaseAmounts(...values: Array) { + return values.reduce((total, value) => total + baseAmount(value), 0n).toString(); +} + +function findPoolForPosition(position: IndexerPoolPosition, pools: RegistryPool[]) { + return pools.find((pool) => pool.pair === position.pairAddress || pool.id === position.poolId || pool.lpToken === position.lpToken); +} + +function denomFromAssetInfo(info: Asset["info"]) { + if ("native_token" in info) return info.native_token.denom; + return info.token.contract_addr; +} + +function rewardFromIncentives(asset: Asset, pool: RegistryPool): PortfolioReward { + const denom = denomFromAssetInfo(asset.info); + const registryAsset = pool.assets.find((candidate) => candidate.id === denom); + const isJuno = denom === "ujuno"; + const junoValue = Number(asset.amount) / 1_000_000; + return { + denom, + symbol: registryAsset?.symbol ?? (isJuno ? "JUNO" : denom), + amount: asset.amount, + valueUsd: null, + valueJuno: isJuno && Number.isFinite(junoValue) ? junoValue : null, + status: baseAmount(asset.amount) > 0n ? "claimable" : "unavailable", + }; +} + +function assetFromIndexer(asset: IndexerAssetAmount, pool: RegistryPool, index: number): PortfolioAssetAmount { + const registryAsset = pool.assets.find((candidate) => candidate.id === asset.denom) ?? pool.assets[index]; + return { + denom: asset.denom, + symbol: asset.symbol ?? registryAsset?.symbol ?? asset.denom, + amount: asset.amount ?? "0", + decimals: registryAsset?.decimals ?? 6, + valueUsd: typeof asset.valueUsd === "number" ? asset.valueUsd : null, + valueJuno: typeof asset.valueJuno === "number" ? asset.valueJuno : null, + priceStatus: asset.priceStatus ?? (typeof asset.priceUsd === "number" ? "fresh" : "missing"), + priceSource: asset.priceSource, + }; +} + +function rewardFromIndexer(asset: IndexerAssetAmount, pool: RegistryPool): PortfolioReward { + const registryAsset = pool.assets.find((candidate) => candidate.id === asset.denom); + return { + denom: asset.denom, + symbol: asset.symbol ?? registryAsset?.symbol ?? asset.denom, + amount: asset.amount ?? "0", + valueUsd: typeof asset.valueUsd === "number" ? asset.valueUsd : null, + valueJuno: typeof asset.valueJuno === "number" ? asset.valueJuno : null, + status: baseAmount(asset.amount) > 0n ? "claimable" : "unavailable", + }; +} + +function positionFromIndexer(position: IndexerPositionWithOptionalRewards, pools: RegistryPool[]): PortfolioPosition | undefined { + const pool = findPoolForPosition(position, pools); + if (!pool) return undefined; + const rewards = (position.claimableRewards ?? position.claimable_rewards ?? position.rewards ?? []).map((reward) => rewardFromIndexer(reward, pool)); + const stakedLpBalance = position.stakedLpBalance ?? position.staked_balance ?? position.bondedBalance ?? position.bonded_balance ?? null; + return { + id: pool.id, + pool, + source: position.isMock || position.dataSource === "mock" ? "mock" : "indexer", + isStale: false, + lpBalance: position.lpBalance, + stakedLpBalance, + shareBps: position.shareBps, + valueUsd: typeof position.valueUsd === "number" ? position.valueUsd : null, + valueJuno: typeof position.valueJuno === "number" ? position.valueJuno : null, + assets: position.assets.map((asset, index) => assetFromIndexer(asset, pool, index)), + rewards, + }; +} + +function positionWithIncentives(position: PortfolioPosition, incentives: IncentivesPoolState | undefined): PortfolioPosition { + if (!incentives) return position; + const pendingRewards = incentives.pendingRewards.map((reward) => rewardFromIncentives(reward, position.pool)); + const rewards = position.rewards.length > 0 ? position.rewards : pendingRewards; + return { + ...position, + stakedLpBalance: position.stakedLpBalance ?? incentives.stakedAmount ?? null, + rewards, + }; +} + +function positionFromFallback(pool: RegistryPool, balances: readonly WalletBalance[], reserve: PoolResponse | undefined): PortfolioPosition | undefined { + const lpBalance = balances.find((balance) => balance.denom === pool.lpToken)?.amount ?? "0"; + const estimate = estimateLpPosition(reserve, lpBalance); + if (!estimate.hasPosition) return undefined; + return { + id: pool.id, + pool, + source: "on-chain", + isStale: false, + lpBalance: estimate.lpBalance, + stakedLpBalance: null, + shareBps: estimate.shareBps, + valueUsd: null, + valueJuno: null, + assets: pool.assets.map((asset, index) => ({ + denom: asset.id, + symbol: asset.symbol, + amount: estimate.underlyingAssets[index]?.amount ?? "0", + decimals: asset.decimals, + valueUsd: null, + valueJuno: null, + priceStatus: "missing", + })), + rewards: [], + }; +} + +export function buildPortfolioSummary(input: { + pools: RegistryPool[]; + balances?: readonly WalletBalance[]; + reservesByPair?: Record; + indexerPositions?: IndexerPositionWithOptionalRewards[]; + incentivesByLpToken?: Record; + preferIndexer?: boolean; +}): PortfolioSummary { + const balances = [...(input.balances ?? [])]; + const positionsById = new Map(); + + if (input.preferIndexer) { + for (const position of input.indexerPositions ?? []) { + const normalized = positionFromIndexer(position, input.pools); + if (normalized) positionsById.set(normalized.id, positionWithIncentives(normalized, input.incentivesByLpToken?.[normalized.pool.lpToken])); + } + } + + for (const pool of input.pools) { + if (positionsById.has(pool.id)) continue; + const fallback = positionFromFallback(pool, balances, input.reservesByPair?.[pool.pair]); + if (fallback) positionsById.set(pool.id, positionWithIncentives(fallback, input.incentivesByLpToken?.[pool.lpToken])); + } + + const positions = Array.from(positionsById.values()).sort((a, b) => a.pool.label.localeCompare(b.pool.label)); + const knownPositionValues = positions.filter((position) => typeof position.valueUsd === "number"); + const knownPositionMarketValues = positions.filter((position) => typeof position.valueUsd === "number" || typeof position.valueJuno === "number"); + const knownPositionJunoValues = positions.filter((position) => typeof position.valueJuno === "number"); + const rewardRows = positions.flatMap((position) => position.rewards).filter((reward) => reward.status === "claimable"); + const knownRewardValues = rewardRows.filter((reward) => typeof reward.valueUsd === "number"); + const knownRewardMarketValues = rewardRows.filter((reward) => typeof reward.valueUsd === "number" || typeof reward.valueJuno === "number"); + const knownRewardJunoValues = rewardRows.filter((reward) => typeof reward.valueJuno === "number"); + return { + positions, + walletBalances: balances, + totalLpValueUsd: knownPositionValues.length === positions.length && positions.length > 0 ? knownPositionValues.reduce((sum, position) => sum + (position.valueUsd ?? 0), 0) : null, + totalLpValueJuno: knownPositionJunoValues.length === positions.length && positions.length > 0 ? knownPositionJunoValues.reduce((sum, position) => sum + (position.valueJuno ?? 0), 0) : null, + totalClaimableUsd: rewardRows.length > 0 && knownRewardValues.length === rewardRows.length ? knownRewardValues.reduce((sum, reward) => sum + (reward.valueUsd ?? 0), 0) : null, + totalClaimableJuno: rewardRows.length > 0 && knownRewardJunoValues.length === rewardRows.length ? knownRewardJunoValues.reduce((sum, reward) => sum + (reward.valueJuno ?? 0), 0) : null, + missingPositionPrices: positions.length - knownPositionMarketValues.length, + missingRewardPrices: rewardRows.length - knownRewardMarketValues.length, + claimableRewardCount: rewardRows.length, + }; +} + +export function totalLpBalance(position: PortfolioPosition) { + return sumBaseAmounts(position.lpBalance, position.stakedLpBalance); +} diff --git a/frontend/src/lib/risk.test.ts b/frontend/src/lib/risk.test.ts new file mode 100644 index 000000000..ba51e4252 --- /dev/null +++ b/frontend/src/lib/risk.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import type { RegistryPool } from "../config/registry"; +import { assessAssetRisk, assessPoolRisk, assessRouteRisk, riskSummary } from "./risk"; + +const verifiedPool: RegistryPool = { + id: "verified", + label: "JUNO / ATOM", + pair: "juno1pair", + lpToken: "factory/juno1pair/lp", + type: "xyk", + feeBps: 30, + assets: [ + { kind: "native", id: "ujuno", symbol: "JUNO", decimals: 6, logoURI: "https://example.com/juno.svg", verified: true }, + { kind: "ibc", id: "ibc/atom", symbol: "ATOM", decimals: 6, logoURI: "https://example.com/atom.svg", verified: true }, + ], + explorer: "https://example.com/pair", + enabled: true, + status: "active", + source: "registry", + verified: true, +}; + +const factoryPool: RegistryPool = { + ...verifiedPool, + id: "factory", + label: "UNKNOWN / TEST", + source: "factory", + verified: false, + assets: [ + { kind: "native", id: "factory/juno1creator/unknown", symbol: "UNKNOWN", decimals: 6 }, + { kind: "ibc", id: "uatom", symbol: "BADATOM", decimals: 6 }, + ], +}; + +describe("risk classification", () => { + it("marks curated assets and pools verified", () => { + const assetRisk = assessAssetRisk(verifiedPool.assets[0]); + expect(assetRisk.verified).toBe(true); + expect(assetRisk.requiresAcknowledgement).toBe(false); + expect(assetRisk.badges.map((badge) => badge.id)).toContain("verified"); + + const poolRisk = assessPoolRisk(verifiedPool); + expect(poolRisk.verified).toBe(true); + expect(poolRisk.requiresAcknowledgement).toBe(false); + expect(riskSummary(poolRisk)).toBe("Verified curated pool and assets."); + }); + + it("flags factory-discovered assets, fallback metadata, and denom mismatches", () => { + const risk = assessPoolRisk(factoryPool); + expect(risk.requiresAcknowledgement).toBe(true); + expect(risk.badges.map((badge) => badge.id)).toEqual(expect.arrayContaining([ + "unverified-pool", + "unverified-token", + "factory-discovered", + "missing-logo", + "decimals-fallback", + "denom-mismatch", + ])); + }); + + it("adds thin liquidity warnings when reserve data is available", () => { + const risk = assessPoolRisk(verifiedPool, { assets: [{ amount: "999999" }, { amount: "1000000" }] }); + expect(risk.badges.map((badge) => badge.id)).toContain("thin-liquidity"); + }); + + it("adds pool-type caveat badges for stable and concentrated pools", () => { + const stableRisk = assessPoolRisk({ ...verifiedPool, type: "stable" }); + const pclRisk = assessPoolRisk({ ...factoryPool, type: "concentrated" }); + + expect(stableRisk.badges.map((badge) => badge.id)).toEqual(expect.arrayContaining(["pool-type-stable", "caveated-liquidity-math"])); + expect(stableRisk.requiresAcknowledgement).toBe(false); + expect(pclRisk.badges.map((badge) => badge.id)).toEqual(expect.arrayContaining(["pool-type-concentrated", "caveated-liquidity-math"])); + expect(pclRisk.requiresAcknowledgement).toBe(true); + }); + + it("requires route acknowledgement when any hop is unverified", () => { + const risk = assessRouteRisk({ + id: "route", + hops: [{ pool: factoryPool, offerAsset: factoryPool.assets[0], askAsset: factoryPool.assets[1] }], + operations: [], + }); + expect(risk.requiresAcknowledgement).toBe(true); + }); + + it("hard-blocks explicitly blocked assets without an acknowledgement override", () => { + const blockedAsset = { ...verifiedPool.assets[0], blocked: true }; + const assetRisk = assessAssetRisk(blockedAsset); + const poolRisk = assessPoolRisk({ ...verifiedPool, assets: [blockedAsset, verifiedPool.assets[1]] }); + + expect(assetRisk.blocked).toBe(true); + expect(assetRisk.badges.map((badge) => badge.id)).toContain("denylisted"); + expect(poolRisk.blocked).toBe(true); + }); + + it("defaults missing verification to unverified and surfaces lifecycle risk", () => { + const unknown = { + ...verifiedPool, + status: "experimental" as const, + verified: undefined, + assets: verifiedPool.assets.map((asset) => ({ ...asset, verified: undefined })) as RegistryPool["assets"], + }; + const risk = assessPoolRisk(unknown); + + expect(risk.verified).toBe(false); + expect(risk.requiresAcknowledgement).toBe(true); + expect(risk.badges.map((badge) => badge.id)).toEqual(expect.arrayContaining([ + "unverified-pool", + "unverified-token", + "pool-status-experimental", + ])); + }); +}); diff --git a/frontend/src/lib/risk.ts b/frontend/src/lib/risk.ts new file mode 100644 index 000000000..2eaaf22bb --- /dev/null +++ b/frontend/src/lib/risk.ts @@ -0,0 +1,176 @@ +import type { RegistryAsset, RegistryPool } from "../config/registry"; +import type { SwapRoute } from "./astroport/routes"; +import { getPoolTypeMetadata } from "./pools/poolTypes"; + +export type RiskSeverity = "ok" | "info" | "warning" | "danger"; + +export type RiskBadge = { + id: string; + label: string; + severity: RiskSeverity; + description: string; + requiresAcknowledgement?: boolean; +}; + +export type RiskAssessment = { + verified: boolean; + badges: RiskBadge[]; + requiresAcknowledgement: boolean; + blocked?: boolean; +}; + +export const THIN_LIQUIDITY_WHOLE_TOKEN_THRESHOLD = 1n; + +const DENYLISTED_DENOMS = new Set([ + // Intentionally empty for launch. Keep this central so operators can add known-bad + // denoms without changing transaction components. +]); + +function uniqueBadges(badges: RiskBadge[]): RiskBadge[] { + const seen = new Set(); + return badges.filter((badge) => { + if (seen.has(badge.id)) return false; + seen.add(badge.id); + return true; + }); +} + +function withRequiresAcknowledgement(badges: RiskBadge[], verified: boolean, blocked = false): RiskAssessment { + const unique = uniqueBadges(badges); + return { + verified, + badges: unique, + requiresAcknowledgement: unique.some((badge) => badge.requiresAcknowledgement), + blocked, + }; +} + +function hasKnownBadDenom(asset: RegistryAsset): boolean { + return DENYLISTED_DENOMS.has(asset.id) || Boolean(asset.denomTrace && DENYLISTED_DENOMS.has(asset.denomTrace)); +} + +export function isAssetBlocked(asset: RegistryAsset): boolean { + return asset.blocked === true || hasKnownBadDenom(asset); +} + +function hasDenomMismatch(asset: RegistryAsset): boolean { + if (asset.kind === "ibc") return !asset.id.startsWith("ibc/") || Boolean(asset.denomTrace && asset.denomTrace.startsWith("ibc/")); + if (asset.kind === "native") return asset.id.startsWith("ibc/") || asset.id.startsWith("juno1"); + if (asset.kind === "cw20") return !asset.id.startsWith("juno1"); + return false; +} + +export function assessAssetRisk(asset: RegistryAsset & { verified?: boolean }, options: { factoryDiscovered?: boolean } = {}): RiskAssessment { + const verified = asset.verified === true; + const blocked = isAssetBlocked(asset); + const badges: RiskBadge[] = []; + + if (verified) { + badges.push({ id: "verified", label: "Verified", severity: "ok", description: "Curated registry metadata." }); + } else { + badges.push({ id: "unverified-token", label: "Unverified", severity: "warning", description: "This asset is not in the curated verified list.", requiresAcknowledgement: true }); + } + + if (options.factoryDiscovered && !verified) { + badges.push({ id: "factory-discovered", label: "Discovered", severity: "info", description: "Found on-chain but not reviewed in the curated asset list." }); + } + + if (blocked) { + badges.push({ id: "denylisted", label: "Blocked denom", severity: "danger", description: "This asset is explicitly blocked or its denom is on the known-bad denylist." }); + } + + if (hasDenomMismatch(asset)) { + badges.push({ id: "denom-mismatch", label: "Denom mismatch", severity: "danger", description: "Asset kind and denom/address shape do not match expectations.", requiresAcknowledgement: true }); + } + + if (!asset.logoURI) { + badges.push({ id: "missing-logo", label: "No logo", severity: verified ? "info" : "warning", description: "No curated logo is available for this asset." }); + } + + if (!verified && asset.decimals === 6) { + badges.push({ id: "decimals-fallback", label: "Decimals fallback", severity: "warning", description: "Decimals may be a 6-decimal fallback for unverified factory metadata." }); + } + + return withRequiresAcknowledgement(badges, verified, blocked); +} + +function isThinReserve(amount: string | undefined, decimals: number): boolean { + if (!amount || !/^\d+$/.test(amount)) return false; + return BigInt(amount) < THIN_LIQUIDITY_WHOLE_TOKEN_THRESHOLD * 10n ** BigInt(decimals); +} + +export function assessPoolRisk(pool: RegistryPool, reserves?: { assets?: Array<{ amount?: string }> }): RiskAssessment { + const verified = pool.verified === true && pool.source !== "factory"; + const blocked = pool.status === "blocked" || pool.assets.some(isAssetBlocked); + const badges: RiskBadge[] = []; + const poolType = getPoolTypeMetadata(pool.type); + + badges.push(verified + ? { id: "verified-pool", label: "Verified pool", severity: "ok", description: "Pool is in the curated verified pool list." } + : { id: "unverified-pool", label: "Unverified pool", severity: "warning", description: "This pool has not been reviewed. Check the pool and asset identifiers before transacting.", requiresAcknowledgement: true }); + + if (pool.status !== "active") { + const lifecycleCopy = pool.status === "experimental" + ? "Experimental pool. It is excluded from normal trading and may contain test or thin-liquidity assets." + : pool.status === "deprecated" + ? "Deprecated pool. It is retained for reference but excluded from new trading routes." + : "Blocked pool. Transactions must not be offered by the interface."; + badges.push({ + id: `pool-status-${pool.status}`, + label: pool.status === "experimental" ? "Experimental" : pool.status === "deprecated" ? "Deprecated" : "Blocked", + severity: pool.status === "blocked" ? "danger" : "warning", + description: lifecycleCopy, + requiresAcknowledgement: true, + }); + } + + badges.push({ + id: `pool-type-${pool.type}`, + label: poolType.shortLabel, + severity: pool.type === "xyk" ? "info" : "warning", + description: poolType.description, + requiresAcknowledgement: pool.type === "concentrated" && !verified, + }); + + if (!poolType.supportsProvideSimulation || !poolType.supportsWithdrawSimulation) { + badges.push({ + id: "caveated-liquidity-math", + label: "Caveated liquidity math", + severity: "warning", + description: "The UI does not locally model this pool type's provide/withdraw invariant; unsupported actions are disabled or marked as estimates.", + }); + } + + for (const asset of pool.assets) { + badges.push(...assessAssetRisk(asset, { factoryDiscovered: pool.source === "factory" }).badges.filter((badge) => badge.id !== "verified")); + } + + if (reserves?.assets?.length) { + const thinAssets = pool.assets.filter((asset, index) => isThinReserve(reserves.assets?.[index]?.amount, asset.decimals)); + if (thinAssets.length > 0) { + badges.push({ id: "thin-liquidity", label: "Thin liquidity", severity: "warning", description: `Low reserve detected for ${thinAssets.map((asset) => asset.symbol).join(" / ")}.` }); + } + } + + return withRequiresAcknowledgement(badges, verified, blocked); +} + +export function assessRouteRisk(route: SwapRoute | undefined, reservesByPair?: Record }>): RiskAssessment { + if (!route) return { verified: false, badges: [], requiresAcknowledgement: false }; + const badges = route.hops.flatMap((hop) => assessPoolRisk(hop.pool, reservesByPair?.[hop.pool.pair]).badges); + const verified = route.hops.every((hop) => hop.pool.verified === true && hop.pool.source !== "factory"); + const blocked = route.hops.some((hop) => hop.pool.status === "blocked" || hop.pool.assets.some(isAssetBlocked)); + if (route.hops.length > 1) { + badges.push({ id: "multi-hop", label: "Multi-hop", severity: "info", description: "Route touches multiple pools; review each hop." }); + } + if (route.hops.some((hop) => !getPoolTypeMetadata(hop.pool.type).supportsLocalPriceImpact)) { + badges.push({ id: "contract-simulated-impact", label: "Contract-simulated impact", severity: "info", description: "Stable/PCL routes rely on contract simulation for pricing; the UI does not recompute those invariants locally." }); + } + return withRequiresAcknowledgement(badges, verified, blocked); +} + +export function riskSummary(assessment: RiskAssessment): string { + const actionable = assessment.badges.filter((badge) => badge.severity === "warning" || badge.severity === "danger"); + if (actionable.length === 0) return "Verified curated pool and assets."; + return actionable.map((badge) => badge.label).join(" · "); +} diff --git a/frontend/src/lib/swap/slippage.test.ts b/frontend/src/lib/swap/slippage.test.ts new file mode 100644 index 000000000..e04732aea --- /dev/null +++ b/frontend/src/lib/swap/slippage.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { + calculateMinimumReceived, + calculatePriceImpactBps, + clampSlippageBps, + classifyPriceImpact, + formatBpsPercent, + slippageBpsToMaxSpread, + slippagePercentToBps, +} from "./slippage"; + +describe("slippage math", () => { + it("calculates minimum received with integer base amounts", () => { + expect(calculateMinimumReceived("1000000", 50)).toBe("995000"); + expect(calculateMinimumReceived("123456789", 10)).toBe("123333332"); + expect(calculateMinimumReceived("1", 50)).toBe("0"); + }); + + it("converts selected slippage to max_spread decimals", () => { + expect(slippageBpsToMaxSpread(10)).toBe("0.001"); + expect(slippageBpsToMaxSpread(50)).toBe("0.005"); + expect(slippageBpsToMaxSpread(100)).toBe("0.01"); + expect(slippagePercentToBps(0.25)).toBe(25); + }); + + it("clamps legacy stored 50% slippage to the 5% safety ceiling", () => { + expect(clampSlippageBps(5_000)).toBe(500); + expect(slippagePercentToBps(50)).toBe(500); + }); +}); + +describe("price impact math", () => { + it("derives price impact from quote spread and return amount", () => { + expect(calculatePriceImpactBps({ spreadAmount: "100", returnAmount: "9900" })).toBe(100); + expect(calculatePriceImpactBps({ spreadAmount: "526", returnAmount: "9474" })).toBe(526); + expect(formatBpsPercent(526)).toBe("5.26%"); + }); + + it("classifies warning and high price impact thresholds", () => { + expect(classifyPriceImpact(null)).toBe("none"); + expect(classifyPriceImpact(99)).toBe("none"); + expect(classifyPriceImpact(100)).toBe("warning"); + expect(classifyPriceImpact(499)).toBe("warning"); + expect(classifyPriceImpact(500)).toBe("high"); + expect(classifyPriceImpact(1_499)).toBe("high"); + expect(classifyPriceImpact(1_500)).toBe("extreme"); + }); +}); diff --git a/frontend/src/lib/swap/slippage.ts b/frontend/src/lib/swap/slippage.ts new file mode 100644 index 000000000..51ffec23d --- /dev/null +++ b/frontend/src/lib/swap/slippage.ts @@ -0,0 +1,74 @@ +export const SLIPPAGE_STORAGE_KEY = "juno-dex.slippage-bps"; +export const DEFAULT_SLIPPAGE_BPS = 50; +export const SLIPPAGE_PRESET_BPS = [10, 50, 100] as const; +export const MIN_SLIPPAGE_BPS = 1; +export const MAX_SLIPPAGE_BPS = 500; +export const HIGH_SLIPPAGE_BPS = 100; +export const DANGEROUS_SLIPPAGE_BPS = 300; +export const HIGH_PRICE_IMPACT_BPS = 500; +export const EXTREME_PRICE_IMPACT_BPS = 1_500; + +export type PriceImpactSeverity = "none" | "warning" | "high" | "extreme"; + +export type PriceImpact = { + bps: number; + severity: PriceImpactSeverity; +}; + +export function clampSlippageBps(bps: number): number { + if (!Number.isFinite(bps)) return DEFAULT_SLIPPAGE_BPS; + return Math.min(MAX_SLIPPAGE_BPS, Math.max(MIN_SLIPPAGE_BPS, Math.round(bps))); +} + +export function slippagePercentToBps(percent: number): number { + return clampSlippageBps(percent * 100); +} + +export function slippageBpsToPercent(bps: number): number { + return clampSlippageBps(bps) / 100; +} + +export function formatSlippagePercent(bps: number): string { + const percent = slippageBpsToPercent(bps); + return Number.isInteger(percent) ? percent.toFixed(0) : percent.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""); +} + +export function slippageBpsToMaxSpread(bps: number): string { + const safeBps = BigInt(clampSlippageBps(bps)); + const whole = safeBps / 10_000n; + const fraction = (safeBps % 10_000n).toString().padStart(4, "0").replace(/0+$/, ""); + return fraction ? `${whole}.${fraction}` : whole.toString(); +} + +export function calculateMinimumReceived(returnAmount: string, slippageBps: number): string { + const amount = BigInt(returnAmount || "0"); + const safeBps = BigInt(clampSlippageBps(slippageBps)); + return ((amount * (10_000n - safeBps)) / 10_000n).toString(); +} + +export function calculatePriceImpactBps({ spreadAmount, returnAmount }: { spreadAmount?: string; returnAmount?: string }): number | null { + if (!spreadAmount || !returnAmount) return null; + const spread = BigInt(spreadAmount); + const received = BigInt(returnAmount); + const idealReturn = spread + received; + if (spread <= 0n || idealReturn <= 0n) return 0; + return Number((spread * 10_000n) / idealReturn); +} + +export function classifyPriceImpact(priceImpactBps: number | null): PriceImpactSeverity { + if (priceImpactBps === null || priceImpactBps < 100) return "none"; + if (priceImpactBps < HIGH_PRICE_IMPACT_BPS) return "warning"; + if (priceImpactBps < EXTREME_PRICE_IMPACT_BPS) return "high"; + return "extreme"; +} + +export function getPriceImpact(input: { spreadAmount?: string; returnAmount?: string }): PriceImpact | null { + const bps = calculatePriceImpactBps(input); + if (bps === null) return null; + return { bps, severity: classifyPriceImpact(bps) }; +} + +export function formatBpsPercent(bps: number): string { + const percent = bps / 100; + return `${percent.toFixed(percent >= 10 ? 1 : 2).replace(/0+$/, "").replace(/\.$/, "")}%`; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 000000000..714005fb2 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,46 @@ +import "./polyfills"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { OverlaysManager, ThemeProvider } from "@interchain-ui/react"; +import { BrowserRouter } from "react-router-dom"; +import { App } from "./app/App"; +import { ToastProvider } from "./components/common"; +import { interchainThemeProps, junoCssVars } from "./theme/junoTheme"; +import { CosmosKitProvider } from "./wallet/CosmosKitProvider"; +import "@interchain-ui/react/styles"; +import "./styles/theme.css"; +import "./styles/surfaces/swap.css"; +import "./styles/surfaces/pools.css"; +import "./styles/surfaces/portfolio.css"; +import "./styles/surfaces/create.css"; +import "./styles/surfaces/liquidity.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + staleTime: 15_000, + refetchOnWindowFocus: false, + }, + }, +}); + +createRoot(document.getElementById("root")!).render( + + +
+ + + + + + + + + + +
+
+
, +); diff --git a/frontend/src/mutations/useCreatePoolTx.ts b/frontend/src/mutations/useCreatePoolTx.ts new file mode 100644 index 000000000..f2ef77a87 --- /dev/null +++ b/frontend/src/mutations/useCreatePoolTx.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { RegistryAsset } from "../config/registry"; +import { dexRegistry } from "../config/registry"; +import { createPairMessage, extractCreatedPairAddress, type CreatePoolConfigOption } from "../lib/createPool"; +import { resolveSigningClient, type SigningClientSource } from "../lib/cosmjs/clients"; +import { invalidateDexTxQueries, type TxResult, useTxRunner } from "../tx/useTxRunner"; +import type { ExecuteInstruction } from "../lib/cosmjs/fees"; + +type CreatePoolTxResult = TxResult & { pairAddress?: string }; + +export type CreatePoolTxVariables = { + assets: [RegistryAsset, RegistryAsset]; + option: CreatePoolConfigOption; +}; + +export function buildCreatePoolExecuteInstruction({ assets, option }: CreatePoolTxVariables): ExecuteInstruction { + return { contractAddress: dexRegistry.factory, msg: createPairMessage(assets, option.pairType) }; +} + +export function useCreatePoolTx(signerOrClient: SigningClientSource, sender: string | undefined) { + const queryClient = useQueryClient(); + const txRunner = useTxRunner(); + const mutation = useMutation({ + mutationFn: async (variables) => { + return txRunner.runTx({ + title: "Create pool", + pendingMessage: `Creating ${variables.assets[0].symbol} / ${variables.assets[1].symbol} ${variables.option.id.toUpperCase()} pool on Juno…`, + variables, + broadcast: async (input) => { + const client = await resolveSigningClient(signerOrClient); + if (!client || !sender) throw new Error("Connect a wallet before broadcasting"); + const instruction = buildCreatePoolExecuteInstruction(input); + const result = await client.execute(sender, instruction.contractAddress, instruction.msg, "auto"); + return { ...result, pairAddress: extractCreatedPairAddress(result) }; + }, + successMessage: (_result, { assets, option }) => `Create pool submitted: ${assets[0].symbol} / ${assets[1].symbol} (${option.label}).`, + onSuccess: async () => { + await invalidateDexTxQueries(queryClient, sender); + await queryClient.invalidateQueries({ queryKey: ["factory-pairs", dexRegistry.chainId, dexRegistry.factory] }); + }, + }); + }, + }); + return { ...mutation, txState: txRunner.state, resetTx: txRunner.reset }; +} diff --git a/frontend/src/mutations/useIncentivesTx.ts b/frontend/src/mutations/useIncentivesTx.ts new file mode 100644 index 000000000..8e36e06ac --- /dev/null +++ b/frontend/src/mutations/useIncentivesTx.ts @@ -0,0 +1,67 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { RegistryPool } from "../config/registry"; +import { createClaimRewardsMessage, createStakeLpExecute, createUnstakeLpMessage, getIncentivesContractAddress } from "../lib/incentives"; +import { resolveSigningClient, type SigningClientSource } from "../lib/cosmjs/clients"; +import { applyConfirmedExactBalanceDeltas, invalidateDexTxQueries, useTxRunner } from "../tx/useTxRunner"; +import { formatAmount } from "../lib/format/amounts"; +import type { ExecuteInstruction } from "../lib/cosmjs/fees"; + +type IncentivesAction = "stake" | "unstake" | "claim"; + +export type IncentivesVariables = { + action: IncentivesAction; + pool: RegistryPool; + amount?: string; +}; + +export function buildIncentivesExecuteInstruction({ action, pool, amount }: IncentivesVariables): ExecuteInstruction { + const incentivesAddress = getIncentivesContractAddress(); + if (!incentivesAddress) throw new Error("Incentives contract is not configured"); + const { msg, funds } = buildIncentivesExecute(pool, action, amount); + return { contractAddress: incentivesAddress, msg: msg as Record, funds }; +} + +export function useIncentivesTx(signerOrClient: SigningClientSource, sender: string | undefined) { + const queryClient = useQueryClient(); + const txRunner = useTxRunner(); + const mutation = useMutation({ + mutationFn: async (variables: IncentivesVariables) => { + return txRunner.runTx({ + title: incentivesActionTitle(variables.action), + pendingMessage: `${incentivesActionTitle(variables.action)} for ${variables.pool.label}…`, + variables, + broadcast: async (input) => { + const client = await resolveSigningClient(signerOrClient); + if (!client || !sender) throw new Error("Connect a wallet before broadcasting"); + const instruction = buildIncentivesExecuteInstruction(input); + return client.execute(sender, instruction.contractAddress, instruction.msg, "auto", undefined, [...(instruction.funds ?? [])]); + }, + successMessage: (_result, { action, pool, amount }) => `${incentivesActionTitle(action)} confirmed for ${pool.label}${amount ? `: ${formatAmount(amount, 6)} LP tokens` : ""}.`, + onSuccess: (_result, { pool, action, amount }) => { + if (amount && action !== "claim") applyConfirmedExactBalanceDeltas(queryClient, sender, [{ denom: pool.lpToken, amount: `${action === "stake" ? "-" : ""}${amount}` }]); + invalidateDexTxQueries(queryClient, sender, pool); + void queryClient.invalidateQueries({ queryKey: ["incentives", pool.lpToken] }); + }, + }); + }, + }); + return { ...mutation, txState: txRunner.state, resetTx: txRunner.reset }; +} + +export function buildIncentivesExecute(pool: RegistryPool, action: IncentivesAction, amount?: string) { + if (action === "stake") { + if (!amount) throw new Error("Enter an LP amount to stake"); + return createStakeLpExecute(pool, amount); + } + if (action === "unstake") { + if (!amount) throw new Error("Enter an LP amount to unstake"); + return { msg: createUnstakeLpMessage(pool, amount), funds: [] }; + } + return { msg: createClaimRewardsMessage(pool), funds: [] }; +} + +function incentivesActionTitle(action: IncentivesAction) { + if (action === "stake") return "Stake LP"; + if (action === "unstake") return "Unstake LP"; + return "Claim rewards"; +} diff --git a/frontend/src/mutations/useProvideLiquidityTx.ts b/frontend/src/mutations/useProvideLiquidityTx.ts new file mode 100644 index 000000000..6a2a80b2a --- /dev/null +++ b/frontend/src/mutations/useProvideLiquidityTx.ts @@ -0,0 +1,45 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { RegistryPool } from "../config/registry"; +import { createProvideLiquidityMessage } from "../lib/astroport/messages"; +import { resolveSigningClient, type SigningClientSource } from "../lib/cosmjs/clients"; +import { applyConfirmedExactBalanceDeltas, invalidateDexTxQueries, useTxRunner } from "../tx/useTxRunner"; +import { formatAmount } from "../lib/format/amounts"; +import type { ExecuteInstruction } from "../lib/cosmjs/fees"; + +export type ProvideLiquidityVariables = { + pool: RegistryPool; + amounts: [string, string]; + slippageTolerance?: string; + minLpToReceive?: string; +}; + +export function buildProvideLiquidityExecuteInstruction({ pool, amounts, slippageTolerance, minLpToReceive }: ProvideLiquidityVariables): ExecuteInstruction { + const { msg, funds } = createProvideLiquidityMessage(pool.assets, amounts, slippageTolerance, minLpToReceive); + return { contractAddress: pool.pair, msg, funds }; +} + +export function useProvideLiquidityTx(signerOrClient: SigningClientSource, sender: string | undefined) { + const queryClient = useQueryClient(); + const txRunner = useTxRunner(); + const mutation = useMutation({ + mutationFn: async (variables: ProvideLiquidityVariables) => { + return txRunner.runTx({ + title: "Add liquidity", + pendingMessage: `Providing liquidity to ${variables.pool.label}…`, + variables, + broadcast: async (input) => { + const client = await resolveSigningClient(signerOrClient); + if (!client || !sender) throw new Error("Connect a wallet before broadcasting"); + const instruction = buildProvideLiquidityExecuteInstruction(input); + return client.execute(sender, instruction.contractAddress, instruction.msg, "auto", undefined, [...(instruction.funds ?? [])]); + }, + successMessage: (_result, { pool, amounts }) => `Liquidity confirmed for ${pool.label}: ${formatAmount(amounts[0], pool.assets[0].decimals)} ${pool.assets[0].symbol} / ${formatAmount(amounts[1], pool.assets[1].decimals)} ${pool.assets[1].symbol}.`, + onSuccess: (_result, { pool, amounts }) => { + applyConfirmedExactBalanceDeltas(queryClient, sender, pool.assets.flatMap((asset, index) => asset.id === "ujuno" || asset.kind === "cw20" ? [] : [{ denom: asset.id, amount: `-${amounts[index]}` }])); + return invalidateDexTxQueries(queryClient, sender, pool); + }, + }); + }, + }); + return { ...mutation, txState: txRunner.state, resetTx: txRunner.reset }; +} diff --git a/frontend/src/mutations/useSwapTx.ts b/frontend/src/mutations/useSwapTx.ts new file mode 100644 index 000000000..34d98c3ed --- /dev/null +++ b/frontend/src/mutations/useSwapTx.ts @@ -0,0 +1,64 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { RegistryAsset, RegistryPool } from "../config/registry"; +import { dexRegistry } from "../config/registry"; +import { createCw20SwapSendMessage, createSwapMessage } from "../lib/astroport/messages"; +import { createCw20RouterSwapSendMessage, createRouterSwapMessage, type SwapRoute } from "../lib/astroport/routes"; +import { resolveSigningClient, type SigningClientSource } from "../lib/cosmjs/clients"; +import { applyConfirmedExactBalanceDeltas, invalidateDexTxQueries, type TxResult, useTxRunner } from "../tx/useTxRunner"; +import { formatAmount } from "../lib/format/amounts"; +import type { ExecuteInstruction } from "../lib/cosmjs/fees"; + +export type SwapTxVariables = { + pool?: RegistryPool; + route: SwapRoute; + offerAsset: RegistryAsset; + askAsset: RegistryAsset; + amount: string; + maxSpread: string; + minimumReceive: string; + source: "pair" | "router"; +}; + +export function buildSwapExecuteInstruction({ pool, route, offerAsset, askAsset, amount, maxSpread, minimumReceive, source }: SwapTxVariables): ExecuteInstruction { + if (source === "pair") { + const directPool = pool ?? route.hops[0]?.pool; + if (!directPool) throw new Error("Direct swap route is missing its pair contract"); + if (offerAsset.kind === "cw20") { + return { contractAddress: offerAsset.id, msg: createCw20SwapSendMessage(directPool.pair, askAsset, amount, maxSpread) }; + } + const { msg, funds } = createSwapMessage(offerAsset, askAsset, amount, maxSpread); + return { contractAddress: directPool.pair, msg, funds }; + } + if (!dexRegistry.router) throw new Error("Router contract is not configured"); + if (offerAsset.kind === "cw20") { + return { contractAddress: offerAsset.id, msg: createCw20RouterSwapSendMessage(dexRegistry.router, route, amount, maxSpread, minimumReceive) }; + } + const { msg, funds } = createRouterSwapMessage(route, offerAsset, amount, maxSpread, minimumReceive); + return { contractAddress: dexRegistry.router, msg, funds }; +} + +export function useSwapTx(signerOrClient: SigningClientSource, sender: string | undefined) { + const queryClient = useQueryClient(); + const txRunner = useTxRunner(); + const mutation = useMutation({ + mutationFn: async (variables: SwapTxVariables) => { + return txRunner.runTx({ + title: "Swap", + pendingMessage: `Swapping ${variables.offerAsset.symbol} for ${variables.askAsset.symbol} on Juno…`, + variables, + broadcast: async (input) => { + const client = await resolveSigningClient(signerOrClient); + if (!client || !sender) throw new Error("Connect a wallet before broadcasting"); + const instruction = buildSwapExecuteInstruction(input); + return client.execute(sender, instruction.contractAddress, instruction.msg, "auto", undefined, [...(instruction.funds ?? [])]); + }, + successMessage: (_result, { amount, minimumReceive, offerAsset, askAsset }) => `Swap confirmed: ${formatAmount(amount, offerAsset.decimals)} ${offerAsset.symbol} for at least ${formatAmount(minimumReceive, askAsset.decimals)} ${askAsset.symbol}.`, + onSuccess: (_result, { pool, route, offerAsset, amount }) => { + if (offerAsset.kind !== "cw20" && offerAsset.id !== "ujuno") applyConfirmedExactBalanceDeltas(queryClient, sender, [{ denom: offerAsset.id, amount: `-${amount}` }]); + return invalidateDexTxQueries(queryClient, sender, pool ?? route.hops[0]?.pool); + }, + }); + }, + }); + return { ...mutation, txState: txRunner.state, resetTx: txRunner.reset }; +} diff --git a/frontend/src/mutations/useWithdrawLiquidityTx.ts b/frontend/src/mutations/useWithdrawLiquidityTx.ts new file mode 100644 index 000000000..b023a169e --- /dev/null +++ b/frontend/src/mutations/useWithdrawLiquidityTx.ts @@ -0,0 +1,46 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { Coin } from "@cosmjs/stargate"; +import type { RegistryPool } from "../config/registry"; +import type { Asset } from "../lib/generated/Pair.types"; +import { createWithdrawLiquidityMessage } from "../lib/astroport/messages"; +import { resolveSigningClient, type SigningClientSource } from "../lib/cosmjs/clients"; +import { applyConfirmedExactBalanceDeltas, invalidateDexTxQueries, useTxRunner } from "../tx/useTxRunner"; +import { formatAmount } from "../lib/format/amounts"; +import type { ExecuteInstruction } from "../lib/cosmjs/fees"; + +export type WithdrawLiquidityVariables = { + pool: RegistryPool; + lpAmount: string; + minAssetsToReceive?: Asset[]; +}; + +export function buildWithdrawLiquidityExecuteInstruction({ pool, lpAmount, minAssetsToReceive }: WithdrawLiquidityVariables): ExecuteInstruction { + const funds: Coin[] = [{ denom: pool.lpToken, amount: lpAmount }]; + return { contractAddress: pool.pair, msg: createWithdrawLiquidityMessage(minAssetsToReceive), funds }; +} + +export function useWithdrawLiquidityTx(signerOrClient: SigningClientSource, sender: string | undefined) { + const queryClient = useQueryClient(); + const txRunner = useTxRunner(); + const mutation = useMutation({ + mutationFn: async (variables: WithdrawLiquidityVariables) => { + return txRunner.runTx({ + title: "Remove liquidity", + pendingMessage: `Withdrawing liquidity from ${variables.pool.label}…`, + variables, + broadcast: async (input) => { + const client = await resolveSigningClient(signerOrClient); + if (!client || !sender) throw new Error("Connect a wallet before broadcasting"); + const instruction = buildWithdrawLiquidityExecuteInstruction(input); + return client.execute(sender, instruction.contractAddress, instruction.msg, "auto", undefined, [...(instruction.funds ?? [])]); + }, + successMessage: (_result, { pool, lpAmount }) => `Withdrawal confirmed for ${pool.label}: ${formatAmount(lpAmount, 6)} LP tokens.`, + onSuccess: (_result, { pool, lpAmount }) => { + applyConfirmedExactBalanceDeltas(queryClient, sender, [{ denom: pool.lpToken, amount: `-${lpAmount}` }]); + return invalidateDexTxQueries(queryClient, sender, pool); + }, + }); + }, + }); + return { ...mutation, txState: txRunner.state, resetTx: txRunner.reset }; +} diff --git a/frontend/src/polyfills.ts b/frontend/src/polyfills.ts new file mode 100644 index 000000000..b7fcfa132 --- /dev/null +++ b/frontend/src/polyfills.ts @@ -0,0 +1,14 @@ +import { Buffer } from "buffer"; + +if (typeof globalThis.Buffer === "undefined") { + globalThis.Buffer = Buffer; +} + +if (typeof globalThis.process === "undefined") { + globalThis.process = { + env: {}, + browser: true, + version: "", + nextTick: (callback: () => void) => Promise.resolve().then(callback), + } as typeof globalThis.process; +} diff --git a/frontend/src/queries/useDexRegistry.ts b/frontend/src/queries/useDexRegistry.ts new file mode 100644 index 000000000..ea5fdaf4f --- /dev/null +++ b/frontend/src/queries/useDexRegistry.ts @@ -0,0 +1,45 @@ +import { useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { configuredPools, dexRegistry, isPoolTradeable } from "../config/registry"; +import { isE2EMode } from "../e2e/mocks"; +import { queryFactoryConfig, queryFactoryPairs } from "../lib/astroport/queries"; +import { factoryFeeBpsByPairType, mergeDiscoveredPools, queryAllFactoryPairs } from "../lib/astroport/poolDiscovery"; + +export function useDexRegistry() { + const discovery = useQuery({ + queryKey: ["factory-pairs", dexRegistry.chainId, dexRegistry.factory], + queryFn: async () => { + const [pairs, configResult] = await Promise.all([ + queryAllFactoryPairs(queryFactoryPairs), + queryFactoryConfig().catch(() => undefined), + ]); + return { pairs, feeBpsByPairType: factoryFeeBpsByPairType(configResult?.pair_configs) }; + }, + staleTime: 60_000, + refetchInterval: 5 * 60_000, + retry: 2, + }); + + const pools = useMemo( + () => { + const merged = discovery.data + ? mergeDiscoveredPools(discovery.data.pairs, configuredPools, discovery.data.feeBpsByPairType) + : configuredPools.map((pool) => ({ ...pool, source: "registry" as const })); + // The committed registry intentionally contains no public markets. The + // isolated E2E build promotes its deterministic fixtures so browser + // tests can exercise transaction flows without weakening production. + return isE2EMode() + ? merged.map((pool) => ({ + ...pool, + status: "active" as const, + enabled: true, + verified: true, + assets: pool.assets.map((asset) => ({ ...asset, verified: true })) as typeof pool.assets, + })) + : merged.filter(isPoolTradeable); + }, + [discovery.data], + ); + + return { registry: dexRegistry, pools, discovery }; +} diff --git a/frontend/src/queries/useIncentives.ts b/frontend/src/queries/useIncentives.ts new file mode 100644 index 000000000..34107dd84 --- /dev/null +++ b/frontend/src/queries/useIncentives.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import type { RegistryPool } from "../config/registry"; +import { queryIncentivesPoolState } from "../lib/incentives"; + +export function useIncentivesPool(pool: RegistryPool | undefined, walletAddress: string | undefined) { + return useQuery({ + queryKey: ["incentives", pool?.lpToken, walletAddress], + enabled: Boolean(pool), + staleTime: 20_000, + retry: false, + queryFn: async () => { + if (!pool) throw new Error("pool is required"); + return queryIncentivesPoolState(pool, walletAddress); + }, + }); +} diff --git a/frontend/src/queries/usePools.ts b/frontend/src/queries/usePools.ts new file mode 100644 index 000000000..8f04ef4f4 --- /dev/null +++ b/frontend/src/queries/usePools.ts @@ -0,0 +1,94 @@ +import { useQueries, useQuery } from "@tanstack/react-query"; +import type { RegistryPool } from "../config/registry"; +import type { SwapRoute } from "../lib/astroport/routes"; +import { queryPairPool } from "../lib/astroport/queries"; +import { loadPoolActivity, loadPoolCandles, loadPoolMetrics, loadWalletIndexerData, type DataAccessState, type PoolCandleRange } from "../lib/data-access/indexerFallback"; +import type { IndexerCandleInterval } from "../lib/indexer/types"; +import type { PoolMetricsByPair } from "../lib/pools/poolList"; + +export function usePoolReserves(pool: RegistryPool | undefined) { + return useQuery({ + queryKey: ["pool", pool?.pair], + enabled: Boolean(pool), + queryFn: async () => { + if (!pool) throw new Error("pool is required"); + return queryPairPool(pool.pair); + }, + }); +} + +export function useRouteReserves(route: SwapRoute | undefined) { + const queries = useQueries({ + queries: (route?.hops ?? []).map((hop) => ({ + queryKey: ["pool", hop.pool.pair], + queryFn: () => queryPairPool(hop.pool.pair), + staleTime: 15_000, + })), + }); + + return Object.fromEntries( + (route?.hops ?? []).flatMap((hop, index) => queries[index]?.data ? [[hop.pool.pair, queries[index].data]] : []), + ); +} + +export function usePoolMetrics(pools: RegistryPool[]) { + const query = useQuery({ + queryKey: ["pool-metrics", pools.map((pool) => pool.pair).join(",")], + enabled: pools.length > 0, + staleTime: 30_000, + retry: 1, + queryFn: () => loadPoolMetrics(pools), + }); + + return { + ...query, + data: query.data?.data ?? ({} as PoolMetricsByPair), + access: query.data?.state as DataAccessState | undefined, + }; +} + +export function usePoolCandles(pool: RegistryPool | undefined, options: { interval?: IndexerCandleInterval; range?: PoolCandleRange; limit?: number } = {}) { + const interval = options.interval ?? "1h"; + const range = options.range ?? "7d"; + const limit = options.limit ?? 200; + const query = useQuery({ + queryKey: ["pool-candles", pool?.pair, interval, range, limit], + enabled: Boolean(pool), + staleTime: 30_000, + retry: 1, + queryFn: () => loadPoolCandles(pool, { interval, range, limit }), + }); + + return { + ...query, + data: query.data?.data ?? [], + access: query.data?.state as DataAccessState | undefined, + }; +} + +export function useWalletIndexerData(address: string | undefined) { + const query = useQuery({ + queryKey: ["wallet-indexer-data", address], + enabled: Boolean(address), + staleTime: 30_000, + retry: 1, + queryFn: () => loadWalletIndexerData(address), + }); + + return { + ...query, + data: query.data?.data ?? { positions: [], history: [] }, + access: query.data?.state as DataAccessState | undefined, + }; +} + +export function usePoolActivity(pool: RegistryPool | undefined, limit = 10) { + const query = useQuery({ + queryKey: ["pool-activity", pool?.pair, limit], + enabled: Boolean(pool), + staleTime: 15_000, + retry: 1, + queryFn: () => loadPoolActivity(pool, limit), + }); + return { ...query, data: query.data?.data ?? [], access: query.data?.state as DataAccessState | undefined }; +} diff --git a/frontend/src/queries/useSwapQuote.test.ts b/frontend/src/queries/useSwapQuote.test.ts new file mode 100644 index 000000000..c01642426 --- /dev/null +++ b/frontend/src/queries/useSwapQuote.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { selectBestRouteQuote, type RouteQuote } from "./useSwapQuote"; +import type { SwapRoute } from "../lib/astroport/routes"; + +function quote(returnAmount: string, hops: number): RouteQuote { + return { + offer_amount: "50", + return_amount: returnAmount, + spread_amount: "0", + commission_amount: "0", + source: hops === 1 ? "pair" : "router", + mode: "exact-in", + route: { id: `${returnAmount}-${hops}`, hops: Array.from({ length: hops }) as SwapRoute["hops"], operations: [] }, + }; +} + +describe("selectBestRouteQuote", () => { + it("selects the highest output across direct and router quotes", () => { + expect(selectBestRouteQuote([quote("100", 1), quote("125", 2), quote("110", 3)])?.return_amount).toBe("125"); + }); + + it("uses the shorter route as a tie-breaker", () => { + expect(selectBestRouteQuote([quote("100", 3), quote("100", 1)])?.route.hops).toHaveLength(1); + }); + + it("selects the lowest required input for exact-out quotes", () => { + const expensive = { ...quote("100", 1), offer_amount: "80", mode: "exact-out" as const }; + const cheap = { ...quote("100", 2), offer_amount: "60", mode: "exact-out" as const }; + expect(selectBestRouteQuote([expensive, cheap], "exact-out")?.offer_amount).toBe("60"); + }); +}); diff --git a/frontend/src/queries/useSwapQuote.ts b/frontend/src/queries/useSwapQuote.ts new file mode 100644 index 000000000..393b118dd --- /dev/null +++ b/frontend/src/queries/useSwapQuote.ts @@ -0,0 +1,122 @@ +import { useEffect, useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import type { RegistryAsset, RegistryPool } from "../config/registry"; +import { queryReverseSwapSimulation, queryRouterReverseSimulation, queryRouterSimulation, querySwapSimulation, type SwapQuoteMode } from "../lib/astroport/queries"; +import { findSwapRoutes, type SwapRoute } from "../lib/astroport/routes"; + +export const SWAP_QUOTE_DEBOUNCE_MS = 300; +export const SWAP_QUOTE_TTL_MS = 30_000; +export const SWAP_QUOTE_REFRESH_INTERVAL_MS = 15_000; + +export type RouteQuote = { + route: SwapRoute; + offer_amount: string; + return_amount: string; + spread_amount: string; + commission_amount: string; + source: "pair" | "router"; + mode: SwapQuoteMode; + previewUnavailable?: boolean; + errors?: string[]; +}; + +function isPositiveAmount(amount: string) { + return /^\d+$/.test(amount) && BigInt(amount) > 0n; +} + +function useDebouncedValue(value: T, delayMs: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + useEffect(() => { + const timer = window.setTimeout(() => setDebouncedValue(value), delayMs); + return () => window.clearTimeout(timer); + }, [delayMs, value]); + return debouncedValue; +} + +async function quoteRoute(route: SwapRoute, amount: string, mode: SwapQuoteMode): Promise { + if (route.hops.length === 1) { + const [hop] = route.hops; + if (mode === "exact-out") { + const quote = await queryReverseSwapSimulation(hop.pool.pair, hop.offerAsset, hop.askAsset, amount); + return { route, source: "pair", mode, offer_amount: quote.offer_amount, return_amount: amount, spread_amount: quote.spread_amount, commission_amount: quote.commission_amount }; + } + const quote = await querySwapSimulation(hop.pool.pair, hop.offerAsset, hop.askAsset, amount); + return { route, source: "pair", mode, offer_amount: amount, ...quote }; + } + + if (mode === "exact-out") { + const quote = await queryRouterReverseSimulation(route.operations, amount); + return { route, source: "router", mode, offer_amount: quote.amount, return_amount: amount, spread_amount: "0", commission_amount: "0" }; + } + + const quote = await queryRouterSimulation(route.operations, amount); + return { route, source: "router", mode, offer_amount: amount, return_amount: quote.amount, spread_amount: "0", commission_amount: "0" }; +} + +export function selectBestRouteQuote(quotes: RouteQuote[], mode: SwapQuoteMode = "exact-in"): RouteQuote | undefined { + return [...quotes].sort((a: RouteQuote, b: RouteQuote) => { + if (mode === "exact-out") { + const inputDiff = BigInt(a.offer_amount) - BigInt(b.offer_amount); + if (inputDiff > 0n) return 1; + if (inputDiff < 0n) return -1; + } else { + const outputDiff = BigInt(b.return_amount) - BigInt(a.return_amount); + if (outputDiff > 0n) return 1; + if (outputDiff < 0n) return -1; + } + return a.route.hops.length - b.route.hops.length; + })[0]; +} + +export function useSwapQuote(pools: RegistryPool[], offerAsset: RegistryAsset | undefined, askAsset: RegistryAsset | undefined, amount: string, mode: SwapQuoteMode = "exact-in", maxHops = 3) { + const debouncedAmount = useDebouncedValue(amount, SWAP_QUOTE_DEBOUNCE_MS); + const query = useQuery({ + queryKey: ["swap-route-quote", mode, pools.map((pool) => pool.pair).join(","), offerAsset?.id, askAsset?.id, debouncedAmount, maxHops], + enabled: Boolean(pools.length && offerAsset && askAsset && isPositiveAmount(debouncedAmount)), + queryFn: async () => { + const routes = findSwapRoutes(pools, offerAsset, askAsset, maxHops); + if (routes.length === 0) throw new Error("No route found for this token pair"); + + const attempts = await Promise.allSettled(routes.map((route) => quoteRoute(route, debouncedAmount, mode))); + const quotes = attempts.flatMap((attempt) => attempt.status === "fulfilled" ? [attempt.value] : []); + const errors = attempts.flatMap((attempt) => attempt.status === "rejected" ? [attempt.reason instanceof Error ? attempt.reason.message : String(attempt.reason)] : []); + const best = selectBestRouteQuote(quotes, mode); + if (!best) throw new Error(errors.length ? `Route preview unavailable: ${errors.join("; ")}` : "Route preview unavailable"); + return { ...best, errors: errors.length ? errors : undefined }; + }, + staleTime: SWAP_QUOTE_TTL_MS, + refetchInterval: SWAP_QUOTE_REFRESH_INTERVAL_MS, + refetchIntervalInBackground: false, + }); + + const quoteUpdatedAt = query.dataUpdatedAt || 0; + const [isExpired, setIsExpired] = useState(false); + + // One timer per quote, firing once at its TTL. A repeating tick here would + // re-render every consumer of this hook once a second for the whole session. + useEffect(() => { + if (!quoteUpdatedAt) { + setIsExpired(false); + return; + } + const remainingMs = quoteUpdatedAt + SWAP_QUOTE_TTL_MS - Date.now(); + if (remainingMs <= 0) { + setIsExpired(true); + return; + } + setIsExpired(false); + const timer = window.setTimeout(() => setIsExpired(true), remainingMs); + return () => window.clearTimeout(timer); + }, [quoteUpdatedAt]); + + const isDebouncing = amount !== debouncedAmount; + + return useMemo(() => ({ + ...query, + debouncedAmount, + isDebouncing, + quoteUpdatedAt, + isExpired, + refreshQuote: query.refetch, + }), [query, debouncedAmount, isDebouncing, quoteUpdatedAt, isExpired]); +} diff --git a/frontend/src/queries/useWalletBalances.ts b/frontend/src/queries/useWalletBalances.ts new file mode 100644 index 000000000..13b1a47b7 --- /dev/null +++ b/frontend/src/queries/useWalletBalances.ts @@ -0,0 +1,119 @@ +import { useMemo } from "react"; +import type { Coin } from "@cosmjs/stargate"; +import { useQuery } from "@tanstack/react-query"; +import { enabledPools, type RegistryAsset, type RegistryPool } from "../config/registry"; +import { e2eBalances, isE2EMode } from "../e2e/mocks"; +import { DEFAULT_DECIMALS, resolveAssetMetadata } from "../lib/assets/assetMetadata"; +import { getReadonlyStargateClient } from "../lib/cosmjs/clients"; + +export const walletBalancesQueryKey = (address: string | undefined) => ["balances", address] as const; + +export type ResolvedDenom = { + denom: string; + symbol: string; + decimals: number; + name?: string; + denomTrace?: string; + logoURI?: string; + source: "registry" | "lp" | "chain-registry" | "raw"; + poolId?: string; + poolLabel?: string; +}; + +export type WalletBalance = ResolvedDenom & { + amount: string; + isKnownDenom: boolean; +}; + +function denomForAsset(asset: RegistryAsset) { + return asset.id; +} + +export function getKnownBalanceDenoms(pools: RegistryPool[] = enabledPools): string[] { + return Array.from(new Set(pools.flatMap((pool) => [...pool.assets.map(denomForAsset), pool.lpToken]))); +} + +export function resolveDenom(denom: string, pools: RegistryPool[] = enabledPools): ResolvedDenom { + for (const pool of pools) { + const asset = pool.assets.find((candidate) => denomForAsset(candidate) === denom); + if (asset) { + return { + denom, + symbol: asset.symbol, + name: asset.name, + decimals: asset.decimals, + denomTrace: asset.denomTrace, + logoURI: asset.logoURI, + source: "registry", + poolId: pool.id, + poolLabel: pool.label, + }; + } + + if (pool.lpToken === denom) { + return { + denom, + symbol: `${pool.assets.map((asset) => asset.symbol).join("/")} LP`, + decimals: DEFAULT_DECIMALS, + source: "lp", + poolId: pool.id, + poolLabel: pool.label, + }; + } + } + + const metadata = resolveAssetMetadata(denom); + return { + denom, + symbol: metadata.symbol, + name: metadata.name, + decimals: metadata.decimals, + denomTrace: metadata.denomTrace, + logoURI: metadata.logoURI, + source: metadata.source === "chain-registry" ? "chain-registry" : "raw", + }; +} + +function mergeKnownDenoms(coins: readonly Coin[], pools: RegistryPool[]): WalletBalance[] { + const coinMap = new Map(coins.map((coin) => [coin.denom, coin.amount])); + const knownDenoms = getKnownBalanceDenoms(pools); + const rows: WalletBalance[] = knownDenoms.map((denom) => ({ + ...resolveDenom(denom, pools), + amount: coinMap.get(denom) ?? "0", + isKnownDenom: true, + })); + + for (const coin of coins) { + if (!coinMap.has(coin.denom)) continue; + if (knownDenoms.includes(coin.denom)) continue; + rows.push({ ...resolveDenom(coin.denom, pools), amount: coin.amount, isKnownDenom: false }); + } + + return rows; +} + +export function getWalletBalanceAmount(balances: readonly WalletBalance[] | undefined, denom: string): string | undefined { + return balances?.find((balance) => balance.denom === denom)?.amount; +} + +export function useWalletBalances(address: string | undefined, pools: RegistryPool[] = enabledPools) { + const query = useQuery({ + queryKey: walletBalancesQueryKey(address), + enabled: Boolean(address), + queryFn: async () => { + if (!address) return []; + if (isE2EMode()) return e2eBalances(pools); + const client = await getReadonlyStargateClient(); + const coins = await client.getAllBalances(address); + return mergeKnownDenoms(coins, pools); + }, + refetchInterval: 30_000, + staleTime: 10_000, + }); + + return useMemo(() => ({ + ...query, + nativeAndPoolBalances: query.data?.filter((balance) => balance.isKnownDenom) ?? [], + byDenom: new Map((query.data ?? []).map((balance) => [balance.denom, balance])), + }), [query]); +} diff --git a/frontend/src/settings/SlippageSettingsContext.test.tsx b/frontend/src/settings/SlippageSettingsContext.test.tsx new file mode 100644 index 000000000..3cad02982 --- /dev/null +++ b/frontend/src/settings/SlippageSettingsContext.test.tsx @@ -0,0 +1,30 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { SLIPPAGE_STORAGE_KEY } from "../lib/swap/slippage"; +import { SlippageSettingsProvider, useSlippageSettings } from "./SlippageSettingsContext"; + +function SlippageProbe() { + const { slippageBps, formattedSlippagePercent, setSlippageBps } = useSlippageSettings(); + return ( +
+ {slippageBps}:{formattedSlippagePercent}% + +
+ ); +} + +describe("SlippageSettingsProvider", () => { + beforeEach(() => window.localStorage.clear()); + + it("migrates a legacy persisted 50% tolerance to the 5% safety ceiling", () => { + window.localStorage.setItem(SLIPPAGE_STORAGE_KEY, "5000"); + render(); + expect(screen.getByLabelText("slippage").textContent).toBe("500:5%"); + }); + + it("never persists a value above the current ceiling", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: /set legacy value/i })); + expect(window.localStorage.getItem(SLIPPAGE_STORAGE_KEY)).toBe("500"); + }); +}); diff --git a/frontend/src/settings/SlippageSettingsContext.tsx b/frontend/src/settings/SlippageSettingsContext.tsx new file mode 100644 index 000000000..66d8a8ee2 --- /dev/null +++ b/frontend/src/settings/SlippageSettingsContext.tsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useMemo, useState, type ReactNode } from "react"; +import { + DEFAULT_SLIPPAGE_BPS, + SLIPPAGE_STORAGE_KEY, + clampSlippageBps, + formatSlippagePercent, + slippageBpsToMaxSpread, + slippageBpsToPercent, + slippagePercentToBps, +} from "../lib/swap/slippage"; + +type SlippageSettings = { + slippageBps: number; + slippagePercent: number; + maxSpread: string; + setSlippageBps: (bps: number) => void; + setSlippagePercent: (percent: number) => void; + formattedSlippagePercent: string; +}; + +const SlippageSettingsContext = createContext(undefined); + +function readStoredSlippageBps(): number { + if (typeof window === "undefined") return DEFAULT_SLIPPAGE_BPS; + const stored = window.localStorage.getItem(SLIPPAGE_STORAGE_KEY); + if (!stored) return DEFAULT_SLIPPAGE_BPS; + return clampSlippageBps(Number(stored)); +} + +export function SlippageSettingsProvider({ children }: { children: ReactNode }) { + const [slippageBps, setSlippageBpsState] = useState(readStoredSlippageBps); + + const setSlippageBps = (nextBps: number) => { + const safeBps = clampSlippageBps(nextBps); + setSlippageBpsState(safeBps); + if (typeof window !== "undefined") window.localStorage.setItem(SLIPPAGE_STORAGE_KEY, String(safeBps)); + }; + + const value = useMemo(() => ({ + slippageBps, + slippagePercent: slippageBpsToPercent(slippageBps), + maxSpread: slippageBpsToMaxSpread(slippageBps), + setSlippageBps, + setSlippagePercent: (percent: number) => setSlippageBps(slippagePercentToBps(percent)), + formattedSlippagePercent: formatSlippagePercent(slippageBps), + }), [slippageBps]); + + return {children}; +} + +export function useSlippageSettings() { + const context = useContext(SlippageSettingsContext); + if (!context) throw new Error("useSlippageSettings must be used within SlippageSettingsProvider"); + return context; +} diff --git a/frontend/src/styles/surfaces/create.css b/frontend/src/styles/surfaces/create.css new file mode 100644 index 000000000..27380ff80 --- /dev/null +++ b/frontend/src/styles/surfaces/create.css @@ -0,0 +1,67 @@ +/* Surface overrides: create. Cascades after theme.css — scoped, page-specific rules only. */ + +.create-pool-page { + width: 100%; + max-width: none; +} + +.create-pool-page > .swap-card { + max-width: none; +} + +/* --- Eyebrow-led header: no billboard title --- */ +.create-pool-page > h2 { + margin: 0 0 10px; + font-size: 1.25rem; + line-height: 1.15; + letter-spacing: 0; + color: var(--cream); +} +.create-pool-page > p:not(.eyebrow) { + max-width: 72ch; + color: var(--cream-mute); + font-size: 0.9rem; +} + +/* Inner card section heading stays quiet + structural */ +.create-pool-page .swap-card-header h2 { + font-size: 1.05rem; + font-weight: 500; + color: var(--cream); +} + +/* --- Mono machine-voice labels across the form --- */ +.create-pool-page dt { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--cream-faint); +} + +/* Guardrails / duplicate notices read as quiet mono footnotes */ +.create-pool-page .create-guardrails { + margin-top: 10px; +} + +.create-pool-page .empty-state.compact strong { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--coral); +} +.create-pool-page .empty-state.compact ul { + margin: 6px 0 0; + padding-left: 18px; + color: var(--cream-mute); + font-size: 0.85rem; +} + +@media (max-width: 640px) { + .create-pool-page > h2 { + font-size: 1.15rem; + } +} diff --git a/frontend/src/styles/surfaces/liquidity.css b/frontend/src/styles/surfaces/liquidity.css new file mode 100644 index 000000000..b500fc393 --- /dev/null +++ b/frontend/src/styles/surfaces/liquidity.css @@ -0,0 +1,69 @@ +/* Surface overrides: liquidity. Cascades after theme.css — scoped, page-specific rules only. */ + + +/* --- Machine-voice labels on the LP action/position cards --- */ +/* .action-card covers add / remove / incentives; .lp-position-panel covers */ +/* the position + portfolio cards. Scoped to component classes so the same */ +/* styling applies on both the Liquidity page and pool-detail routes. */ +.action-card dt, +.lp-position-panel dt { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--cream-faint); +} + +/* Big mono numerics for LP position metrics */ +.lp-position-panel .metric-card > span { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--cream-faint); +} +.lp-position-panel .metric-card strong { + font-family: var(--font-mono); + font-weight: 700; + font-size: 1.4rem; + line-height: 1.12; + letter-spacing: -0.01em; + color: var(--cream); +} +.lp-position-panel .metric-card code { + font-size: 0.75rem; + color: var(--cream-faint); +} + +/* Add-liquidity quote block: key/value rows as quiet mono lines */ +.action-card .quote-card p { + margin: 0; + font-family: var(--font-mono); + font-size: 0.82rem; + line-height: 1.6; + color: var(--cream-dim); +} +.action-card .quote-card strong { + color: var(--cream-faint); + font-weight: 700; +} + +/* Incentives reward lists read as quiet mono rows */ +.incentives-panel h4 { + margin: 0 0 6px; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--coral); +} +.incentives-panel .asset-list { + margin: 0; + padding-left: 18px; + font-family: var(--font-mono); + font-size: 0.82rem; + color: var(--cream-dim); +} diff --git a/frontend/src/styles/surfaces/pools.css b/frontend/src/styles/surfaces/pools.css new file mode 100644 index 000000000..023076299 --- /dev/null +++ b/frontend/src/styles/surfaces/pools.css @@ -0,0 +1,363 @@ +/* Surface overrides: pools. Cascades after theme.css — scoped, page-specific rules only. */ + +/* Kill the billboard card: the mockup leads with an eyebrow + the table card, + sitting directly on the gridded page background. */ +.pools-page { + border: 0; + background: transparent; + box-shadow: none; + padding: 0; + width: 100%; + max-width: none; + display: grid; + gap: 0; +} + +.pools-page-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 18px; +} + +/* Mono count line with a leading coral tick, matching the topbar coordinate. */ +.pools-nodes-eyebrow { + display: inline-flex; + align-items: center; + gap: 9px; + margin: 0; +} +.pools-nodes-eyebrow::before { + content: ""; + width: 7px; + height: 1px; + display: inline-block; + background: var(--coral); +} + +/* + Provide — primary-tinted action, top-right. */ +.pools-provide-link { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 14px; + border: 1px solid var(--coral); + border-radius: var(--r-sm, 3px); + background: var(--coral); + color: var(--maroon-deep); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + box-shadow: var(--glow-soft); + white-space: nowrap; +} +.pools-provide-link:hover { color: var(--maroon-deep); background: var(--coral-bright); } +.pools-provide-link span { font-size: 0.95rem; line-height: 1; } + +.pools-page .pool-list-shell { margin-top: 0; } + +/* Restrained control strip: quiet mono labels, hairline inputs on one line. */ +.pools-page .pool-list-controls { + grid-template-columns: minmax(190px, 1.6fr) repeat(4, minmax(118px, 1fr)); + gap: 10px; + padding: 2px 2px 4px; +} +.pools-page .pool-list-controls label { + gap: 5px; + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--cream-faint); +} +.pools-page .pool-list-controls input, +.pools-page .pool-list-controls select { + padding: 8px 10px; + font-size: 0.85rem; +} + +/* Column layout: POOL NODE / TVL / APR / 24H VOL / YOUR POSITION. */ +.pools-page .pool-table-header, +.pools-page .pool-row { + grid-template-columns: minmax(240px, 1.6fr) repeat(4, minmax(90px, 1fr)); +} + +/* Header: quiet mono; sortable buttons match the same quiet tone until hover. */ +.pools-page .pool-table-header button { + color: var(--cream-faint); + font-weight: 700; +} +.pools-page .pool-table-header button:hover { color: var(--coral); } + +/* Only the position column is right-aligned (headers + values), per the mockup. */ +.pools-page .pool-table-header > :last-child, +.pools-page .pool-position { + text-align: right; + justify-items: end; +} + +/* Numerics: mono; per-cell labels hidden on desktop (column headers carry them). */ +.pools-page .pool-metric strong, +.pools-page .pool-position strong { + font-family: var(--font-mono); + font-weight: 500; + font-size: 0.95rem; +} +.pools-page .pool-metric > span, +.pools-page .pool-position > span { display: none; } +.pools-page .pool-metric small { + font-size: 0.75rem; + letter-spacing: 0.1em; + color: var(--cream-faint); +} + +/* APR in coral. */ +.pools-page .pool-metric-apr strong { color: var(--coral); } + +/* Pool identity: paired glyphs + display-face label on one line. */ +.pools-page .pool-title-line { + display: inline-flex; + align-items: center; + gap: 10px; + flex-wrap: nowrap; + max-width: 100%; +} +.pools-page .pool-title-copy strong { + font-family: var(--font-display); + font-weight: 600; + font-size: 0.9rem; +} + +@media (max-width: 860px) { + /* Rows collapse to a stack — restore per-cell labels + left alignment. */ + .pools-page .pool-metric > span, + .pools-page .pool-position > span { display: block; } + .pools-page .pool-table-header > :last-child, + .pools-page .pool-position { + text-align: left; + justify-items: start; + } +} + +/* ================================================================== */ +/* Pool detail — a clean vertical document. Eyebrow-led header, mono */ +/* stat grid, hairline-delimited sections. No redundant in-page nav. */ +/* ================================================================== */ +.pool-detail-page { + width: 100%; + max-width: none; + display: grid; + gap: 28px; +} + +/* --- Header: eyebrow + pair name + terse meta, quiet back link --- */ +.pool-detail-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} +.pool-detail-lead { display: grid; gap: 9px; min-width: 0; } +.pool-detail-page .pool-detail-lead h2 { + margin: 0; + font-size: 1.5rem; + line-height: 1.08; + letter-spacing: 0; +} +.pool-detail-eyebrow { margin: 0; } +.pool-detail-eyebrow::before { + content: ""; + display: inline-block; + width: 14px; + height: 1px; + margin-right: 9px; + vertical-align: middle; + background: var(--coral); +} +.pool-detail-meta { + margin: 0; + font-family: var(--font-mono); + font-size: 0.78rem; + letter-spacing: 0.04em; + color: var(--cream-faint); +} +.pool-detail-back { + flex: none; + align-self: flex-start; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--cream-mute); + white-space: nowrap; +} +.pool-detail-back:hover { color: var(--coral); } + +/* --- Consolidated contract identity: one compact mono block --- */ +.pool-detail-identity { + display: grid; + gap: 11px; + padding: 16px 18px; + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} +.pool-detail-identity .eyebrow { margin: 0 0 1px; } +.pool-identity-row { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} +.pool-identity-label { + flex: none; + width: 84px; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--cream-faint); +} +.pool-identity-row > code { + flex: 1; + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--cream-dim); +} +.pool-identity-row .identifier-disclosure { flex: 1; min-width: 0; } +.pool-identity-row button { flex: none; } + +/* --- Sections: consistent rhythm, spaced-mono coral header labels --- */ +.pool-detail-page .pool-detail-section { + display: grid; + gap: 14px; + margin: 0; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} +.pool-detail-page .pool-detail-section > h3 { + margin: 0; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--coral); +} + +/* --- Mono stat grid: small uppercase labels, big Space Mono numbers --- */ +.pool-detail-page .metrics-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} +.pool-detail-page .metric-card { + flex: none; + padding: 16px; + gap: 8px; + border-radius: var(--r-sm, 3px); +} +.pool-detail-page .metric-card > span, +.pool-detail-page .metric-card .pool-asset-heading { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--cream-faint); +} +.pool-detail-page .metric-card strong { + font-family: var(--font-mono); + font-weight: 700; + font-size: 1.5rem; + line-height: 1.12; + letter-spacing: -0.01em; + color: var(--cream); +} +.pool-detail-page .metric-card code, +.pool-detail-page .metric-card small { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--cream-faint); +} +/* Honest unavailable / long-text values sit quietly, not as a hero number */ +.pool-detail-page .metric-value-muted { + font-size: 0.9rem; + font-weight: 400; + letter-spacing: 0; + color: var(--cream-mute); +} +.pool-detail-page .metric-value-long { + font-size: 0.9rem; + font-weight: 500; + letter-spacing: 0; + line-height: 1.5; +} +.pool-detail-page .pool-metrics-copy { + margin: 0; + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--cream-mute); +} +.pool-service-note { + margin: 0; + padding: 12px 14px; + border-left: 2px solid var(--signal-warn); + background: color-mix(in srgb, var(--signal-warn) 8%, transparent); + color: var(--text-secondary); +} +.pool-technical-details { + padding: 16px 18px; + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} +.pool-technical-details > summary { + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 700; + color: var(--text-secondary); +} +.pool-technical-details[open] > summary { margin-bottom: 16px; } +.pool-technical-details .pool-detail-identity { margin-bottom: 14px; } + +/* --- Definition-list sections: mono uppercase keys, quiet values --- */ +.pool-detail-page .pool-detail-section dt { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--cream-faint); +} +.pool-detail-page .pool-detail-section .quote-detail-value { + color: var(--cream-dim); +} + +/* --- Add + Remove liquidity side by side --- */ +.pool-detail-page .liquidity-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + align-items: start; +} + +@media (max-width: 860px) { + .pool-detail-header { flex-direction: column; } + .pool-detail-page .metrics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .pool-detail-page .liquidity-grid { grid-template-columns: 1fr; } +} +@media (max-width: 560px) { + .pool-detail-page .metrics-grid { grid-template-columns: 1fr; } +} diff --git a/frontend/src/styles/surfaces/portfolio.css b/frontend/src/styles/surfaces/portfolio.css new file mode 100644 index 000000000..38b6e5019 --- /dev/null +++ b/frontend/src/styles/surfaces/portfolio.css @@ -0,0 +1,79 @@ +/* Surface overrides: portfolio. Cascades after theme.css — scoped, page-specific rules only. */ + +/* --- Eyebrow-led header: kill the billboard H1 + paragraph --- */ +.portfolio-hero { + align-items: flex-end; + gap: 18px; +} +.portfolio-hero-lead { + display: grid; + gap: 2px; +} +.portfolio-page h2 { + margin: 0; + font-size: 1.2rem; + line-height: 1.15; + letter-spacing: 0; + color: var(--cream); +} + +/* --- Machine-voice labels sit above big mono numerics --- */ +.portfolio-page .metric-card > span, +.portfolio-page dt { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--cream-faint); +} + +.portfolio-page .metric-card strong, +.portfolio-page .metric-value { + font-family: var(--font-mono); + font-weight: 700; + font-size: 1.55rem; + line-height: 1.12; + letter-spacing: -0.01em; + color: var(--cream); +} + +/* Honest "USD price unavailable" / "missing" states — quiet, not a hero */ +.portfolio-page .metric-value-muted { + font-size: 0.95rem; + font-weight: 400; + letter-spacing: 0; + color: var(--cream-mute); +} + +/* Supporting captions read as quiet mono footnotes */ +.portfolio-page .metric-card small, +.portfolio-page .metric-card code { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--cream-faint); +} + +/* Detail rows: mono values, quiet inline notes */ +.portfolio-page .quote-detail-value { + color: var(--cream-dim); +} +.portfolio-page .quote-detail-value small { + color: var(--cream-faint); +} + +/* Position + balances panels: compact structural heading */ +.portfolio-page .lp-position-header h3 { + font-size: 1.15rem; +} + +@media (max-width: 640px) { + .portfolio-hero { + flex-direction: column; + align-items: flex-start; + } + .portfolio-page .metric-card strong, + .portfolio-page .metric-value { + font-size: 1.35rem; + } +} diff --git a/frontend/src/styles/surfaces/swap.css b/frontend/src/styles/surfaces/swap.css new file mode 100644 index 000000000..e24fc7ee1 --- /dev/null +++ b/frontend/src/styles/surfaces/swap.css @@ -0,0 +1,175 @@ +/* Surface overrides: swap. Cascades after theme.css — scoped, page-specific rules only. */ + +/* ── Field eyebrow labels (YOU SEND / YOU RECEIVE) + balance line ── */ +/* Mockup structure: the label + `bal …` sit on one full-width strip at the + top of the box, and the amount + token selector share the row below. The + topline is lifted out of the input column and pinned across the whole box + so the token pill can never crowd up against the balance. */ +.swap-page-grid .swap-card .asset-amount-card { + position: relative; + padding-top: 38px; +} +.swap-page-grid .swap-card .asset-amount-card .form-grid { align-items: center; } +.swap-page-grid .swap-card .token-amount-topline { + position: absolute; + top: 14px; + left: 16px; + right: 16px; + display: flex; + flex-wrap: nowrap; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} +.swap-page-grid .swap-card .token-amount-topline > span:first-child { + flex: none; + white-space: nowrap; + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--cream-faint); +} +.swap-page-grid .swap-card .token-balance { + flex: none; + white-space: nowrap; + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.02em; + color: var(--cream-faint); +} + +/* Big amount + optional USD subvalue */ +.swap-page-grid .swap-card .asset-amount-card .token-amount-row input { + font-size: clamp(1.6rem, 5vw, 2rem); + letter-spacing: -0.02em; +} +.swap-page-grid .swap-card .asset-amount-card .token-amount-actions { + justify-content: flex-start; +} +.swap-page-grid .swap-card .fiat-hint { + margin: 0; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.02em; + color: var(--cream-faint); +} + +/* ── Flip button seated on the seam between the cards ─────────────── */ +.swap-page-grid .swap-card .swap-amount-stack { position: relative; } +.swap-page-grid .swap-card .swap-direction { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + z-index: 2; + margin: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 34px !important; + min-width: 34px !important; + height: 34px !important; + padding: 0 !important; + border: 1px solid var(--coral-a35) !important; + border-radius: var(--r-sm, 3px) !important; + background: var(--maroon) !important; + color: var(--coral) !important; + box-shadow: none !important; + outline: none !important; + cursor: pointer; + transition: border-color 120ms var(--ease-mech), background 120ms var(--ease-mech), color 120ms var(--ease-mech); +} +.swap-page-grid .swap-card .swap-direction:hover { + border-color: var(--coral) !important; + background: var(--coral-a08) !important; + color: var(--coral-bright) !important; +} +.swap-page-grid .swap-card .swap-direction-icon { + display: block; + width: 15px; + height: 15px; +} + +/* ── Quote: the rate is the only always-visible line; the rest expands ── */ +.swap-page-grid .swap-card .quote-disclosure { + margin: 0; +} +.swap-page-grid .swap-card .quote-rate { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 2px; + cursor: pointer; + list-style: none; + color: var(--cream); + font-family: var(--font-mono); + font-size: 0.8rem; +} +.swap-page-grid .swap-card .quote-rate::-webkit-details-marker { display: none; } +.swap-page-grid .swap-card .quote-disclosure-chevron { + flex: none; + width: 7px; + height: 7px; + border-right: 1.5px solid var(--cream-mute); + border-bottom: 1.5px solid var(--cream-mute); + transform: rotate(45deg) translate(-2px, -2px); + transition: transform 120ms cubic-bezier(0.2, 0, 0, 1); +} +.swap-page-grid .swap-card .quote-disclosure[open] .quote-disclosure-chevron { + transform: rotate(-135deg) translate(-2px, -2px); +} + +.swap-page-grid .swap-card .quote-rows { + display: grid; + gap: 10px; + margin: 14px 2px 2px; + font-family: var(--font-mono); +} +.swap-page-grid .swap-card .quote-rows > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.swap-page-grid .swap-card .quote-rows dt { + color: var(--cream-mute); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.04em; +} +.swap-page-grid .swap-card .quote-row-value { + min-width: 0; + text-align: right; + color: var(--cream); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 400; + overflow-wrap: anywhere; + background: transparent; +} +.swap-page-grid .swap-card .quote-row-value.route-value { color: var(--coral); } +.swap-page-grid .swap-card .quote-row-value.status-ok { color: var(--signal-ok); background: transparent; } +.swap-page-grid .swap-card .quote-row-value.status-warn { color: var(--signal-warn); background: transparent; } +.swap-page-grid .swap-card .quote-row-value.status-danger { color: var(--coral-bright); background: transparent; } + +/* Expandable extra detail sits below the primary rows */ +.swap-page-grid .swap-card .quote-card .quote-toggle { margin-top: 12px; } +.swap-page-grid .swap-card .quote-details { margin-top: 10px; } + +/* ── Slippage preset chips ───────────────────────────────────────── */ +.swap-review-rows { + display: grid; + gap: 10px; + margin: 0; +} +.swap-review-rows > div { + display: flex; + justify-content: space-between; + gap: 16px; +} +.transaction-review { display: grid; gap: 16px; } +.transaction-review-description { margin: 0; line-height: 1.5; } +.transaction-review-disclosure summary { cursor: pointer; color: var(--coral); font-weight: 700; } +.transaction-review-disclosure code { overflow-wrap: anywhere; } diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css new file mode 100644 index 000000000..6b40caa4b --- /dev/null +++ b/frontend/src/styles/theme.css @@ -0,0 +1,2507 @@ +@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap"); + +:root { + color-scheme: dark; + font-family: var(--font-body, Montserrat, Gotham, "Helvetica Neue", Arial, sans-serif); + color: var(--text-primary, #ffebd2); + background: var(--surface-void, #0a0203); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +* { box-sizing: border-box; } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +html { background: var(--surface-void); } + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + overflow-x: hidden; + background: + linear-gradient(rgba(255, 123, 124, 0.055) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 123, 124, 0.055) 1px, transparent 1px), + radial-gradient(circle at 92% 6%, rgba(255, 123, 124, 0.18), transparent 28rem), + linear-gradient(180deg, #100405 0%, #0a0203 58%, #1b0708 100%); + background-size: 32px 32px, 32px 32px, auto, auto; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: -1; + background: repeating-linear-gradient(0deg, rgba(255, 235, 210, 0.018) 0 1px, transparent 1px 4px); + opacity: 0.5; +} + +a { color: var(--coral); text-decoration: none; } +a:hover { color: var(--coral-bright); } +button, input, select { font: inherit; } +button { letter-spacing: 0; } +code { color: var(--cream-dim); overflow-wrap: anywhere; font-family: var(--font-mono); } + +.dex-shell { + width: 100%; + min-height: 100vh; + display: grid; + grid-template-columns: 216px minmax(0, 1fr); + grid-template-rows: 58px auto 1fr; + margin: 0; + padding: 0; +} + +.app-header { + position: sticky; + top: 0; + z-index: 100; + grid-row: 1 / span 3; + width: 216px; + height: 100vh; + margin: 0; + padding: 20px 14px; + border: 0; + border-right: 1px solid var(--line-hair); + border-radius: 0; + background: var(--void-pure); + box-shadow: none; + display: flex; + flex-direction: column; + gap: 26px; +} + +.header-inner, +.topbar-actions, +.primary-nav, +.contract-strip, +.form-grid, +.metrics-grid, +.liquidity-grid, +.swap-card-header, +.quote-header, +.pool-title-line, +.quick-fill-row, +.token-amount-topline, +.token-amount-row, +.token-amount-actions, +.modal-header, +.toast, +.footer-grid { + display: flex; + gap: 12px; +} + +.header-inner { + align-items: flex-start; + justify-content: flex-start; +} +.mobile-header-account, +.mobile-quick-nav { display: none; } + +.brand-lockup { + min-width: 0; + display: flex; + align-items: center; + gap: 12px; + color: var(--cream); +} + +.brand-logo { + width: 22px; + height: 22px; + object-fit: contain; + filter: drop-shadow(0 0 14px rgba(255, 123, 124, 0.24)); +} + +.brand-lockup { gap: 10px; } +.brand-copy { display: inline-flex; align-items: center; min-width: 0; } +.brand-title { display: inline-flex; align-items: center; gap: 8px; line-height: 1; } +.brand-title img { height: 13px; width: auto; display: block; } +.brand-title span { + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.2em; + color: var(--cream-faint); +} + +.brand-lockup:focus-visible, +.nav-link:focus-visible, +button:focus-visible, +a:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 3px; +} + +.brand-title, +.header-inner h1, +.hero-panel h2, +.panel-page h2, +.stats-chart-panel h3, +.stats-top-pools h3, +.action-card h3 { + margin: 0; + font-family: var(--font-display); + font-weight: 500; + letter-spacing: 0; + color: var(--cream); +} + +.hero-panel h2, +.panel-page h2 { + margin-bottom: 12px; + font-size: clamp(1.5rem, 2.2vw, 2rem); + line-height: 1.08; + letter-spacing: -0.01em; +} + +.context-panel h2, +.swap-card h2 { + font-size: clamp(1.45rem, 2.2vw, 2rem); +} + +.eyebrow { + display: block; + margin: 0 0 8px; + color: var(--coral); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: var(--juno-eyebrow-tracking, 0.18em); + line-height: 1.5; + text-transform: uppercase; +} + +.topbar-actions { + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; +} + +.app-topbar { + position: sticky; + top: 0; + z-index: 90; + grid-column: 2; + grid-row: 1; + height: 58px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 22px; + border-bottom: 1px solid var(--line-hair); + background: var(--maroon-deep); +} + +.topbar-coord { + display: inline-flex; + align-items: center; + gap: 10px; + margin: 0; + color: var(--coral); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 400; + letter-spacing: 0.24em; + text-transform: uppercase; +} + +.topbar-coord::before { + content: ""; + width: 14px; + height: 1px; + display: inline-block; + background: var(--coral); +} + +.primary-nav { + margin-top: 0; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + gap: 2px; +} + +.nav-link, +.mode-tab, +.segmented-control button { + border: 1px solid transparent; + border-radius: var(--r-sm, 3px); + padding: 9px 12px; + color: var(--cream-mute); + background: transparent; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.nav-link { + width: 100%; + display: flex; + align-items: center; + gap: 10px; +} + +.nav-link-icon { + flex: none; + color: var(--cream-faint); + transition: color 0.15s ease; +} + +.nav-link:hover .nav-link-icon { color: var(--cream-mute); } + +.nav-link.active .nav-link-icon, +.nav-link[aria-current="page"] .nav-link-icon { + color: var(--coral); +} + +.nav-link-label { min-width: 0; } + +.nav-link:hover, +.mode-tab:hover, +.segmented-control button:hover { + color: var(--cream); + border-color: var(--line-hair); + background: var(--coral-a04); +} + +.nav-link.active, +.nav-link[aria-current="page"], +.mode-tab.active, +.segmented-control button.active { + color: var(--coral); + border-color: var(--line-soft); + background: var(--coral-a08); + box-shadow: inset 0 0 0 1px rgba(255, 123, 124, 0.03); +} + +.mode-tab.disabled { opacity: 0.42; } + +.icon-button, +.mobile-nav-toggle, +.text-button, +.wallet-inline-action, +.wallet-address-actions a, +.contract-strip button, +.token-amount-actions button, +.state-card button, +.modal-header button, +.toast button, +.quick-fill-row button, +.primary-link, +.secondary-link { + border: 1px solid var(--line-soft); + border-radius: var(--r-sm, 3px); + background: transparent; + color: var(--coral); + padding: 9px 11px; + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.icon-button:hover, +.mobile-nav-toggle:hover, +.text-button:hover, +.wallet-inline-action:hover, +.wallet-address-actions a:hover, +.contract-strip button:hover, +.token-amount-actions button:hover, +.state-card button:hover, +.modal-header button:hover, +.toast button:hover, +.quick-fill-row button:hover, +.secondary-link:hover { + background: var(--coral-a08); + border-color: var(--line-strong); +} + +.mobile-nav-toggle { display: none; } +.mobile-nav-icon, +.mobile-nav-icon::before, +.mobile-nav-icon::after { + width: 18px; + height: 1px; + display: block; + background: currentColor; +} +.mobile-nav-icon { position: relative; } +.mobile-nav-icon::before, +.mobile-nav-icon::after { content: ""; position: absolute; left: 0; } +.mobile-nav-icon::before { top: -5px; } +.mobile-nav-icon::after { top: 5px; } + +.sidebar-network { + margin-top: auto; + display: grid; + gap: 8px; + padding: 0 6px; +} +.sidebar-network:empty { display: none; } + + + + + + + +.settings-panel { + position: absolute; + right: 22px; + top: calc(100% - 8px); + z-index: 200; + width: min(420px, calc(100vw - 32px)); + padding: 16px; + border: 1px solid var(--line-strong); + border-radius: var(--r-md, 5px); + background: rgba(16, 4, 5, 0.98); + box-shadow: var(--shadow-pop, 0 24px 60px rgba(0,0,0,0.6)); +} + +.settings-panel p, +.hero-panel p, +.panel-page p, +.action-card p, +.stats-chart-panel p:not(.eyebrow), +.wallet-history-header p { + color: var(--cream-mute); + line-height: 1.65; +} + +.settings-header { + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.settings-title { color: var(--cream); font-weight: 600; } + +.slippage-settings-group, +.custom-asset-box { + display: grid; + gap: 12px; + margin: 12px 0 14px; + padding: 12px; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: var(--surface-inset); +} + +.slippage-settings-group legend, +.custom-asset-box legend { + color: var(--coral); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 0 6px; +} + +.slippage-presets { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.slippage-preset { + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + padding: 9px 10px; + background: transparent; + color: var(--cream-mute); + cursor: pointer; +} + +.slippage-preset.active { + border-color: var(--line-strong); + background: var(--coral-a08); + color: var(--coral); +} + +.app-main { + grid-column: 2; + grid-row: 3; + min-width: 0; + padding: 28px; + background-color: var(--maroon); + background-image: + linear-gradient(var(--coral-a04) 1px, transparent 1px), + linear-gradient(90deg, var(--coral-a04) 1px, transparent 1px); + background-size: 32px 32px; +} + +.page-grid, +.swap-page-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(340px, 440px); + gap: 22px; + align-items: start; +} + +.swap-page-grid { + grid-template-columns: 440px minmax(0, 1fr); + justify-content: center; + gap: 24px; + width: min(100%, 980px); + max-width: 980px; + margin: 0 auto; +} + +.swap-primary { order: 1; } +.context-panel { order: 2; position: relative; overflow: hidden; } +.context-panel:not(.market-panel)::after { + content: "08"; + position: absolute; + right: 18px; + bottom: -18px; + color: rgba(255, 123, 124, 0.1); + font-family: var(--font-mono); + font-size: 7rem; + line-height: 1; +} + +.market-panel { + display: flex; + flex-direction: column; + gap: 16px; + overflow: visible; +} + +.market-card { + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: var(--surface-card); + box-shadow: var(--shadow-card); + padding: 20px; +} + +.market-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + margin-bottom: 14px; +} + +.market-card-header h2 { + margin: 0; + color: var(--cream); + font-size: 1.25rem; +} + +.market-pair-title { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.market-pair-title p { + margin: 3px 0 0; + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.market-token-mark { + width: 28px; + height: 28px; + display: grid; + place-items: center; + flex: none; + border: 1px solid var(--line-soft); + border-radius: 50%; + background: var(--coral-a08); + color: var(--coral); + font-family: var(--font-mono); + font-weight: 700; +} + +.market-price { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 3px; + text-align: right; +} + +.market-price strong { + color: var(--cream); + font-family: var(--font-display, var(--font-heading)); + font-weight: 500; + font-size: 1.25rem; + letter-spacing: -0.01em; +} + +.market-change { + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.04em; +} + +.market-change.up { color: var(--signal-ok); } +.market-change.down { color: var(--coral-bright, var(--coral)); } + +.market-sparkline { + height: 88px; + padding: 8px 0; +} + +.market-sparkline svg { + display: block; + width: 100%; + height: 100%; + overflow: visible; +} + +.spark-fill { + fill: rgba(255, 123, 124, 0.16); +} + +.spark-line { + fill: none; + stroke: var(--coral); + stroke-width: 1.5; + vector-effect: non-scaling-stroke; +} + +.market-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 14px; +} + +.market-stats span { + display: grid; + gap: 4px; + min-width: 0; +} + +.market-stats small, +.transaction-row small { + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.market-stats strong { + color: var(--cream); + font-family: var(--font-mono); + font-size: 0.8rem; +} + +.transaction-list { + display: grid; + gap: 10px; + margin-top: 14px; +} + +.transaction-row { + display: grid; + grid-template-columns: 16px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + color: var(--cream-dim); + font-family: var(--font-mono); + font-size: 0.75rem; +} + +.transaction-kind { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + font-size: 0.75rem; + line-height: 1; + color: var(--coral); +} + +.transaction-kind.add { color: var(--signal-ok); } + +.transaction-row strong { + min-width: 0; + color: var(--cream-dim); + font-weight: 400; + font-size: inherit; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.hero-panel, +.swap-card, +.panel-page, +.quote-card, +.metric-card, +.action-card, +.tx-card, +.lp-position-panel, +.wallet-history-section, +.price-chart-card { + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: var(--surface-card); + box-shadow: var(--shadow-card); +} + +.hero-panel, +.panel-page { + padding: clamp(22px, 4vw, 40px); +} + +.swap-card { + width: 100%; + max-width: 440px; + padding: 20px; + position: relative; +} +.swap-card-header, +.quote-header, +.pool-title-line { + align-items: center; + justify-content: space-between; + flex-wrap: wrap; +} + +.swap-card-header h2 { + font-size: 1.125rem; + line-height: 1.2; +} + +.swap-settings { + position: relative; +} + +.slippage-icon-button { + height: 34px; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 0 10px; + color: var(--cream-mute); +} + +/* The current tolerance is visible on the trigger, so the quote card doesn't repeat it. */ +.slippage-pill-value { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--cream-dim); +} + +.slippage-icon { + position: relative; + width: 16px; + height: 13px; + display: block; + background: + linear-gradient(var(--coral), var(--coral)) 0 1px / 16px 1px no-repeat, + linear-gradient(var(--coral), var(--coral)) 0 6px / 16px 1px no-repeat, + linear-gradient(var(--coral), var(--coral)) 0 11px / 16px 1px no-repeat; +} + +.slippage-icon::before, +.slippage-icon::after { + content: ""; + position: absolute; + width: 4px; + height: 4px; + border: 1px solid var(--coral); + border-radius: 50%; + background: var(--maroon-deep); +} + +.slippage-icon::before { + left: 3px; + top: -1px; +} + +.slippage-icon::after { + right: 3px; + top: 9px; +} + +.swap-settings .settings-panel { + right: 0; + top: calc(100% + 10px); +} + +.slippage-pill { + border-color: var(--line-soft) !important; + border-radius: var(--r-sm, 3px) !important; + color: var(--coral) !important; + background: var(--coral-a04) !important; + font-family: var(--font-mono) !important; + text-transform: uppercase; +} + +.mode-tabs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin: 14px 0; +} + +.form-grid { align-items: end; } +.asset-amount-card .form-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + align-items: center; + gap: 12px; +} +.field { display: grid; gap: 8px; color: var(--cream-mute); flex: 1; } +label { color: var(--cream-mute); } + +input, +select, +.token-search-input { + width: 100%; + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); + color: var(--cream); + padding: 12px 13px; +} + +input::placeholder { color: var(--cream-faint); } +input:focus, +select:focus, +.token-search-input:focus { + border-color: var(--line-strong); + box-shadow: 0 0 0 3px var(--coral-a12); + outline: none; +} + +.asset-amount-card, +.receive-box { + display: grid; + gap: 6px; + padding: 14px 16px; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: var(--surface-inset); +} + +.asset-amount-card > code { + display: none; +} + +.swap-amount-stack { + position: relative; + display: grid; + gap: 6px; +} + +.asset-card-topline { + justify-content: space-between; + color: var(--cream-mute); + font-family: var(--font-mono); + font-size: 0.75rem; +} + +.asset-card-topline strong, +.estimated-receive, +.pool-metric strong, +.pool-position strong, +.lp-position-header h3, +.wallet-history-header h3, +.chart-summary strong { + color: var(--cream); + font-weight: 600; +} + +.asset-card-token, +.pool-asset-heading, +.token-identity { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.token-identity strong { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.estimated-receive { font-size: clamp(1.6rem, 5vw, 2.2rem); } + +.swap-direction { + position: absolute !important; + top: 50% !important; + left: 50% !important; + z-index: 2 !important; + width: 34px !important; + height: 34px !important; + display: grid !important; + place-items: center !important; + margin: 0 !important; + padding: 0 !important; + transform: translate(-50%, -50%) !important; + border: 1px solid var(--line-strong) !important; + border-radius: var(--r-sm, 3px) !important; + background: var(--maroon) !important; + color: var(--coral) !important; + box-shadow: var(--glow-soft); +} + +.quote-card { + min-height: 220px; + padding: 14px 0 0; + margin-top: 2px; + border: 0; + border-top: 1px solid var(--line-hair); + border-radius: 0; + background: transparent; + box-shadow: none; + transition: opacity 160ms ease; +} +.quote-placeholder { + display: grid; + min-height: 194px; + place-items: center; + padding: 20px; + color: var(--text-muted); + text-align: center; +} + +.quote-card-updating { opacity: 0.66; } + + + +.selected-token-origin { + color: var(--cream-mute); + font-size: 0.75rem; + font-weight: 500; + white-space: nowrap; +} + +@media (prefers-reduced-motion: reduce) { + .quote-card { transition: none; } +} + +.quote-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + border: 0; + background: transparent; + color: var(--coral); + padding: 0; + cursor: pointer; +} + +.quote-toggle .eyebrow { + margin: 0; +} + +.quote-toggle-triangle { + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid var(--cream-mute); + transition: transform 120ms var(--ease-mech); +} + +.quote-toggle[aria-expanded="true"] .quote-toggle-triangle { + transform: rotate(90deg); +} + +dl { margin: 0; display: grid; gap: 10px; } +dl div { display: flex; justify-content: space-between; gap: 12px; } +dt { color: var(--cream-mute); } +dd { margin: 0; font-weight: 400; color: var(--cream-dim); } +.quote-details { min-width: 0; } +.quote-details div { + display: grid; + grid-template-columns: minmax(0, auto) minmax(0, 1fr); + align-items: start; +} +.quote-detail-value { + min-width: 0; + text-align: right; + overflow-wrap: anywhere; + word-break: break-word; + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 400; +} +.quote-detail-value a { overflow-wrap: anywhere; word-break: break-word; } +.quote-card .price-chart-card, +.quote-card .state-card, +.quote-card .empty-state { + box-shadow: none; +} + +.primary-action, +.wallet-button, +.action-card button { + margin-top: 16px; + border: 1px solid var(--coral) !important; + border-radius: var(--r-sm, 3px) !important; + padding: 12px 16px !important; + background: var(--coral) !important; + color: var(--text-on-coral) !important; + box-shadow: var(--glow-soft); + cursor: pointer; + font-family: var(--font-mono) !important; + font-size: 0.78rem !important; + font-weight: 700 !important; + letter-spacing: 0.1em !important; + text-transform: uppercase; +} + +.primary-action:hover, +.wallet-button:hover, +.action-card button:hover { + background: var(--coral-bright) !important; + border-color: var(--coral-bright) !important; +} + +button:disabled { cursor: not-allowed; opacity: 0.48; box-shadow: none; } + +.primary-action:disabled, +.wallet-button:disabled, +.action-card button:disabled { + border-color: var(--text-muted) !important; + background: var(--text-muted) !important; + color: var(--text-on-coral) !important; + opacity: 1; +} +.wallet-button { margin-top: 0; } +.wallet-button:not(.connected) { + min-height: 34px; + padding: 7px 14px !important; + background: var(--surface-inset) !important; + color: var(--coral) !important; + border-color: var(--line-strong) !important; + box-shadow: inset 0 0 0 1px var(--coral-a04) !important; +} +.wallet-button:not(.connected):hover { + background: var(--coral-a08) !important; + color: var(--cream) !important; +} +.wallet-button.connected { + display: inline-flex !important; + align-items: center !important; + gap: 8px !important; + min-height: 32px; + padding: 6px 12px !important; + background: transparent !important; + color: var(--cream) !important; + border-color: var(--line-soft) !important; + box-shadow: none; + text-transform: none !important; + letter-spacing: 0.02em !important; + font-weight: 400 !important; + font-size: 0.75rem !important; +} +.wallet-button.connected:hover { + background: var(--coral-a04) !important; + border-color: var(--line-strong) !important; +} +.wallet-status-dot, +.wallet-connected-dot { + width: 7px; + height: 7px; + flex: none; + border-radius: 50%; + background: var(--coral); + box-shadow: var(--glow-soft); +} + +.wallet-stack { display: grid; gap: 6px; justify-items: end; } +.wallet-account-menu { position: relative; } +.wallet-account-popover { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 100; + display: grid; + gap: 8px; + width: min(300px, calc(100vw - 32px)); + padding: 12px; + border: 1px solid var(--line-soft); + border-radius: var(--r-md, 5px); + background: var(--maroon); + box-shadow: var(--shadow-card); +} +.wallet-account-popover > a, .wallet-account-popover > button { width: 100%; text-align: left; } +.wallet-stack.connected { + display: inline-flex; + align-items: center; + gap: 8px; + justify-items: initial; + padding: 6px 8px; + border: 1px solid var(--line-soft); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} +.wallet-connected-name { color: var(--cream); font-weight: 600; } +.wallet-address-actions { + display: inline-flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} +.wallet-address-actions code { color: var(--cream-dim); } + +.status-pill { + border-radius: var(--r-sm, 3px); + padding: 9px 11px; + font-family: var(--font-mono); + font-size: 0.75rem; + border: 1px solid currentColor; + white-space: nowrap; + background: transparent; +} +.status-ok { color: var(--signal-ok); background: rgba(143, 176, 138, 0.08); } +.status-warn { color: var(--signal-warn); background: rgba(233, 169, 79, 0.08); } +.status-danger, +.error-text { color: var(--coral-hot); } +.success-text { color: var(--signal-ok); } + +.price-impact-warning, +.network-guard-banner, +.empty-state, +.state-card { + border: 1px solid var(--line-soft); + border-radius: var(--r-md, 5px); + background: var(--coral-a08); + color: var(--cream-dim); +} + +.price-impact-warning { margin-top: 12px; padding: 12px; line-height: 1.45; } +.price-impact-danger { display: flex; gap: 10px; align-items: flex-start; border-color: rgba(255, 123, 124, 0.55); background: rgba(198, 72, 74, 0.14); } +.price-impact-danger input { width: auto; margin-top: 0.25rem; } +.network-guard-banner { + grid-column: 2; + grid-row: 2; + margin: 16px 22px 0; + padding: 14px 16px; + box-shadow: var(--shadow-card); +} +.network-guard-banner button { margin-top: 0; } + +.pool-list-shell { + display: grid; + gap: 14px; + margin-top: 22px; +} +.pool-list-controls { + display: grid; + grid-template-columns: minmax(220px, 1.4fr) repeat(4, minmax(150px, 1fr)); + gap: 12px; + align-items: end; +} +.pool-list-controls label { + display: grid; + gap: 6px; + color: var(--cream-mute); + font-size: 0.9rem; +} +.pool-table, +.lp-position-list { + display: grid; + gap: 12px; + margin-top: 8px; +} + +.pool-table { + gap: 0; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: rgba(16, 4, 5, 0.76); + overflow: hidden; +} + +.pool-table-header, +.pool-row { + display: grid; + grid-template-columns: minmax(280px, 1.6fr) repeat(3, minmax(100px, 0.62fr)) minmax(130px, 0.75fr); + gap: 12px; + align-items: center; +} +.pool-table-header, +.stats-top-pool-head, +.wallet-history-row-head { + padding: 0 16px; + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.12em; +} +.pool-table-header { + padding: 12px 20px; + border-bottom: 1px solid var(--line-hair); +} +.pool-table-header button { + border: 0; + background: transparent; + color: var(--coral); + padding: 0; + text-align: left; + cursor: pointer; + text-transform: inherit; + letter-spacing: inherit; + font-weight: 700; +} +.pool-row, +.liquidity-row, +.stats-top-pool-row, +.wallet-history-row { + color: var(--cream); +} +.pool-row { + padding: 14px 20px; + text-decoration: none; + border-bottom: 1px solid var(--line-hair); + cursor: pointer; +} +.pool-row:last-child { border-bottom: 0; } +.pool-row:hover, +.stats-top-pool-row:hover, +.wallet-history-row:hover { + background: rgba(255, 123, 124, 0.045); +} +.stats-top-pool-row:hover, +.wallet-history-row:hover { + border-color: var(--line-soft); +} +.pool-row:focus-visible { + outline: 1px solid var(--coral); + outline-offset: -2px; +} +.liquidity-row, +.stats-top-pool-row, +.wallet-history-row { + padding: 14px 16px; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: rgba(16, 4, 5, 0.76); +} +.liquidity-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(160px, auto) auto; + gap: 14px; + align-items: center; +} +.pool-row p { margin: 4px 0 0; font-size: 0.9rem; } +.pool-main { + min-width: 0; +} + +.pool-title-line { + display: inline-grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + justify-content: start; + gap: 10px; + max-width: 100%; +} + +.pool-token-stack { + display: inline-flex; + align-items: center; + width: 44px; + flex: none; +} + +.pool-token-stack .token-logo-frame, +.pool-token-stack .token-logo-fallback, +.pool-token-stack .token-logo-img { + box-shadow: 0 0 0 2px var(--surface-card); +} + +.pool-token-stack > * + * { + margin-left: -8px; +} +.pool-token-stack > *:last-child { z-index: 1; } + +.pool-title-copy { + min-width: 0; + display: grid; + gap: 3px; +} + +.pool-title-copy strong, +.pool-title-copy small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pool-title-copy strong { + color: var(--cream); +} + +.pool-title-copy small { + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.pool-assets { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 12px; +} +.pool-assets div { + display: grid; + gap: 4px; + padding: 12px; + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} +.pool-assets span { display: inline-flex; align-items: center; gap: 6px; } +.pool-assets img { + width: 20px; + height: 20px; + border-radius: 50%; + object-fit: contain; + background: var(--cream-a10); +} +.pool-assets span, +.pool-assets small, +.pool-meta span, +.pool-metric span, +.pool-position span, +.risk-copy, +.stats-top-pool-row small, +.wallet-history-row small, +.token-selector-trigger-copy small, +.token-row-copy small, +.token-row-meta small, +.token-selector-help, +.empty-token-results, +.chart-loading, +.chart-summary, +.price-chart-subtitle { + color: var(--cream-mute); +} +.pool-meta, +.pool-metric, +.pool-position { + display: grid; + gap: 6px; + min-width: 0; +} +.pool-metric small { color: var(--coral); } + +.stats-dashboard-page, +.scaffold-page, +.create-pool-page { + display: grid; + gap: 22px; +} + +.portfolio-page { + display: grid; + gap: 18px; +} + +.portfolio-hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.portfolio-hero h2 { + margin-bottom: 10px; +} + +.portfolio-hero p:not(.eyebrow) { + max-width: 760px; + margin: 0; +} + +.portfolio-wallet-chip { + display: inline-flex; + align-items: center; + gap: 8px; + flex: none; + max-width: 100%; + padding: 7px 11px; + border: 1px solid var(--line-soft); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} + +.portfolio-wallet-chip span { + width: 7px; + height: 7px; + flex: none; + border-radius: 50%; + background: var(--coral); + box-shadow: var(--glow-soft); +} + +.portfolio-wallet-chip code { + min-width: 0; + color: var(--cream); + font-size: 0.78rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.portfolio-total-grid { + margin-top: 0; +} + +.portfolio-page > .lp-position-panel { + margin-top: 0; +} + +.create-pool-page > .swap-card { + width: 100%; + max-width: 900px; +} + +.create-pool-page .swap-card-header { + align-items: flex-start; +} + +.create-pool-page .status-pill { + max-width: 100%; + white-space: normal; + line-height: 1.45; +} + +.create-pool-page .form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: end; +} + +.create-pool-page .pool-assets { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.create-pool-page .pool-assets div { + min-width: 0; +} + +.create-pool-page .pool-assets strong, +.create-pool-page .pool-assets code { + min-width: 0; + overflow-wrap: anywhere; +} + +.create-custom-asset-box { + padding: 14px; +} + +.custom-asset-toggle { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + margin: 0; + padding: 10px 12px; + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} + +.custom-asset-toggle input { + width: auto; + flex: none; + margin: 0; +} +.stats-hero { display: grid; gap: 14px; } +.stats-source-line, +.hero-actions, +.stats-section-header, +.lp-position-header, +.lp-position-actions, +.wallet-history-header, +.wallet-history-source, +.chart-controls, +.price-chart-header, +.chart-summary { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; +} +.hero-actions, +.wallet-history-source, +.lp-position-actions { justify-content: flex-start; } +.primary-link { + background: var(--coral); + color: var(--maroon-deep); + border-color: var(--coral); + box-shadow: var(--glow-soft); +} +.primary-link:hover { color: var(--maroon-deep); background: var(--coral-bright); } +.secondary-link { background: transparent; color: var(--coral); } +.metric-card, +.action-card { + padding: 18px; + flex: 1 1 260px; + display: grid; + gap: 8px; +} +.metric-card span { color: var(--cream-mute); } +.metric-card strong { font-size: 1.6rem; } +.stats-top-pools { + padding: 18px; +} +.stats-top-pool-table { + display: grid; + gap: 10px; + margin-top: 14px; +} +.stats-top-pool-row { + display: grid; + grid-template-columns: minmax(220px, 1.5fr) repeat(3, minmax(110px, 0.7fr)) minmax(170px, 0.9fr); + gap: 12px; + align-items: center; +} +.stats-top-pool-row div { + display: grid; + gap: 4px; + min-width: 0; +} +.stats-top-pool-head { + padding-top: 0; + padding-bottom: 0; + border: 0; + background: transparent; +} + +.lp-position-panel { + display: grid; + gap: 16px; + margin-top: 22px; + padding: 18px; +} +.lp-position-panel-compact { background: rgba(35, 10, 12, 0.78); } +.lp-position-header { align-items: flex-start; } +.lp-position-header h3, +.wallet-history-header h3 { margin: 0 0 4px; font-size: 1.35rem; } +.lp-position-header p:not(.eyebrow) { margin: 0; color: var(--cream-mute); font-size: 0.95rem; } +.lp-position-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +.lp-underlying-list { + padding: 14px; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: var(--surface-inset); +} +.lp-position-actions .wallet-inline-action { + display: inline-flex; + align-items: center; + margin-top: 0; + text-align: center; +} +.lp-position-skeleton { display: grid; gap: 10px; padding: 14px; } + +.wallet-history-section { + display: grid; + gap: 16px; + margin-top: 26px; + padding: 18px; +} +.wallet-history-header { align-items: flex-start; } +.wallet-history-header p { margin: 0; } +.wallet-history-filters { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 8px; +} +.wallet-history-list { display: grid; gap: 10px; } +.wallet-history-row { + display: grid; + grid-template-columns: minmax(120px, 0.85fr) minmax(130px, 0.8fr) minmax(220px, 1.5fr) minmax(120px, 0.75fr) minmax(150px, 0.85fr); + gap: 12px; + align-items: center; +} +.wallet-history-row-head { + background: transparent; + border: 0; + padding-bottom: 0; +} +.wallet-history-row div { display: grid; gap: 4px; min-width: 0; } +.wallet-history-row code, +.wallet-history-row strong, +.wallet-history-tx-cell a, +.pool-metric strong, +.pool-position strong { + overflow-wrap: anywhere; + word-break: break-word; +} +.wallet-history-tx-cell { grid-template-columns: minmax(0, 1fr) auto; align-items: center; } +.tx-copy-button { + border: 0; + padding: 4px; + background: transparent; + color: var(--coral); + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.75rem; + text-transform: uppercase; +} + + +.market-panel > .price-chart-card { margin: 0; } +.market-panel .price-chart-compact { padding: 20px; } +.market-panel .price-chart-compact .price-chart-header { align-items: flex-start; } +.market-panel .price-chart-compact .chart-plot { min-height: 120px; } +.transaction-row > div { display: grid; gap: 3px; min-width: 0; } +.transaction-row > a { color: var(--coral); font-size: 0.75rem; text-transform: uppercase; } + +.juno-wallet-modal { backdrop-filter: blur(8px); } +.juno-wallet-modal-content { border: 1px solid var(--line-strong) !important; background: var(--surface-card) !important; box-shadow: var(--shadow-pop) !important; } +.juno-wallet-modal-children { background: var(--surface-card) !important; color: var(--cream) !important; } + +.empty-state, +.state-card { + padding: 14px; +} +.state-card strong, +.empty-state strong { + display: block; + color: var(--cream); + margin-bottom: 6px; +} +.state-card p, +.empty-state p { + margin: 0; + color: inherit; +} +.error-state-card { + border-color: rgba(255, 123, 124, 0.4); + color: var(--coral-hot); + background: rgba(198, 72, 74, 0.14); +} +.optional-data-state { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border: 1px dashed var(--line-soft); + color: var(--text-muted); + font-size: 0.82rem; +} +.optional-data-state details { text-align: right; } +.optional-data-state summary { cursor: pointer; color: var(--text-secondary); } +.optional-data-state p { margin: 8px 0; max-width: 42rem; } +.optional-data-timestamp { + margin: 0; + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 0.75rem; +} + +.token-amount-input { + display: grid; + gap: 10px; + flex: 1; + min-width: min(100%, 280px); +} + +.asset-amount-card .token-amount-input { + min-width: 0; + gap: 8px; +} + +.token-amount-topline { + justify-content: space-between; + color: var(--cream-mute); + font-family: var(--font-mono); + font-size: 0.76rem; +} +.token-balance { color: var(--coral); } +.token-amount-row { + display: grid; + grid-template-columns: minmax(0, 52%) minmax(0, 1fr); + align-items: center; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + padding: 10px; + background: var(--surface-inset); + overflow: hidden; +} +.token-amount-row-compact { + grid-template-columns: 1fr; +} + +.asset-amount-card .token-amount-row { + border: 0; + border-radius: 0; + padding: 0; + background: transparent; +} + +.token-amount-row input { + min-width: 0; + border: 0; + background: transparent; + text-align: right; + font-family: var(--font-display); + font-size: clamp(1.35rem, 5vw, 1.875rem); + font-weight: 500; + padding: 6px; +} + +.asset-amount-card .token-amount-row input { + padding: 0; + text-align: left; + font-size: 2rem; + line-height: 1.15; +} +.token-logo-slot, +.token-logo-fallback, +.token-logo-frame, +.token-logo-img { + width: 30px; + height: 30px; + border-radius: 50%; + display: grid; + place-items: center; + border: 1px solid var(--line-soft); + background: var(--coral-a08); + color: var(--coral); + font-family: var(--font-mono); + font-weight: 700; + text-transform: uppercase; + flex: 0 0 auto; +} +.token-logo-sm { width: 24px; height: 24px; font-size: 0.75rem; } +.token-logo-img { object-fit: contain; background: var(--cream-a10); } +.token-logo-frame[data-fallback]::after { content: attr(data-fallback); } +.token-amount-actions { + justify-content: flex-end; + flex-wrap: wrap; +} + +.asset-amount-card .token-amount-actions { + min-height: 0; + margin: 0; +} + +.fiat-hint { + margin-right: auto; + color: var(--cream-mute); + font-size: 0.9rem; +} +.field-error { margin: 0; color: var(--coral-hot); font-size: 0.9rem; } + +.token-selector { min-width: min(100%, 240px); } +.asset-amount-card .token-selector { + min-width: 0; + align-self: end; +} + +.token-selector-compact { + gap: 0; +} + +.asset-amount-card .token-selector-trigger { + min-width: 132px; + max-width: 176px; +} + +.token-selector-trigger { + min-width: 0; + width: 100%; + display: flex; + align-items: center; + gap: 10px; + justify-content: space-between; + border: 1px solid var(--line-hair); + border-radius: var(--r-pill, 999px); + padding: 7px 12px 7px 8px; + background: var(--surface-raised); + color: var(--cream); + cursor: pointer; + text-align: left; + overflow: hidden; +} +.token-selector-trigger:hover { + border-color: var(--line-soft); + background: var(--coral-a04); +} +.token-selector-trigger-copy, +.token-row-copy, +.token-row-meta { + display: grid; + gap: 2px; + min-width: 0; +} +.token-selector-trigger-copy { flex: 1; } +.token-selector-trigger-copy strong, +.token-selector-trigger-copy small, +.token-row-copy strong, +.token-row-copy small, +.token-row-meta strong, +.token-row-meta small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.token-selector.field { + gap: 7px; +} + +.token-selector.field > span { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--cream-mute); +} +.token-selector-modal { + display: grid; + gap: 12px; + margin-top: 16px; +} +.token-list { + display: grid; + gap: 8px; + max-height: min(58vh, 520px); + overflow: auto; + padding-right: 4px; +} +.empty-token-results { + display: grid; + gap: 10px; + margin: 0; + padding: 12px; + border: 1px solid var(--line-hair); + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} +.empty-token-results p { + margin: 0; +} +.empty-token-results button { + width: fit-content; + border: 1px solid var(--coral); + border-radius: var(--r-sm, 3px); + padding: 8px 12px; + background: var(--coral); + color: var(--maroon-deep); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; +} +.empty-token-results button:hover { + background: var(--coral-bright); +} +.token-row { + display: grid; + grid-template-columns: auto 1fr; + align-items: stretch; + gap: 8px; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: rgba(16, 4, 5, 0.72); +} +.token-row.selected { + border-color: var(--line-strong); + background: var(--coral-a08); +} +.token-row.disabled { opacity: 0.55; } +.favorite-button, +.token-row-main { + border: 0; + background: transparent; + color: var(--cream); + cursor: pointer; +} +.favorite-button { + padding: 0 12px; + color: var(--coral); + font-size: 1.1rem; +} +.token-row-main { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px 10px 0; + text-align: left; + min-width: 0; +} +.token-row-main:disabled { cursor: not-allowed; } +.token-row-meta { text-align: right; color: var(--coral); } + +.risk-badge-list { + display: inline-flex; + gap: 6px; + align-items: center; + flex-wrap: wrap; + margin-left: 6px; + vertical-align: middle; +} +.risk-badge { + display: inline-block; + border: 1px solid rgba(233, 169, 79, 0.45); + border-radius: var(--r-sm, 3px); + padding: 1px 6px; + color: var(--signal-warn); + background: rgba(233, 169, 79, 0.08); + font-family: var(--font-mono); + font-size: 0.75rem; + font-style: normal; + vertical-align: middle; + white-space: nowrap; +} +.risk-badge-ok { + color: var(--signal-ok); + border-color: rgba(143, 176, 138, 0.45); + background: rgba(143, 176, 138, 0.08); +} +.risk-badge-info { + color: var(--cream-dim); + border-color: var(--cream-a20); + background: var(--cream-a06); +} +.risk-badge-warning { + color: var(--signal-warn); + border-color: rgba(233, 169, 79, 0.45); + background: rgba(233, 169, 79, 0.1); +} +.risk-badge-danger { + color: var(--coral-hot); + border-color: rgba(255, 123, 124, 0.55); + background: rgba(255, 123, 124, 0.14); +} +.risk-acknowledgement { display: flex; gap: 10px; align-items: flex-start; } +.skip-link { + position: fixed; + top: 8px; + left: 8px; + z-index: 1000; + transform: translateY(-150%); + padding: 10px 14px; + background: var(--cream); + color: var(--maroon); + font-weight: 700; +} +.skip-link:focus { transform: translateY(0); } +.transaction-center { + position: fixed; + right: 16px; + bottom: 16px; + z-index: 30; + width: min(360px, calc(100vw - 32px)); + border: 1px solid var(--line-soft); + border-radius: var(--r-md, 5px); + background: var(--maroon); + box-shadow: var(--shadow-card); +} +.transaction-center summary { cursor: pointer; padding: 12px 14px; color: var(--cream); font-weight: 700; } +.transaction-center ul { display: grid; gap: 10px; max-height: 45vh; overflow: auto; margin: 0; padding: 0 12px 12px; list-style: none; } +.transaction-center li { padding: 10px; border: 1px solid var(--line-soft); background: var(--surface-inset); } +.transaction-center li > div:first-child, .transaction-center-actions { display: flex; justify-content: space-between; gap: 10px; } +.transaction-center li span { color: var(--cream-mute); text-transform: capitalize; } +.transaction-center li p { margin: 6px 0; color: var(--cream-dim); } +.transaction-center-actions { align-items: center; } +.identifier-disclosure { + margin-top: 4px; + color: var(--cream-mute); + font-size: 0.82rem; +} +.identifier-disclosure summary { + cursor: pointer; + color: var(--coral); + font-weight: 700; +} +.identifier-disclosure code { + display: block; + margin-top: 4px; + max-width: 100%; + overflow-wrap: anywhere; + font-size: 0.82rem; +} + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 1000; + display: grid; + place-items: center; + padding: 20px; + background: rgba(10, 2, 3, 0.72); + backdrop-filter: blur(8px); +} +.modal-card { + width: min(520px, 100%); + max-height: min(720px, calc(100vh - 40px)); + overflow: auto; + border: 1px solid var(--line-strong); + border-radius: var(--r-md, 5px); + padding: 20px; + background: var(--surface-card); + box-shadow: var(--shadow-pop, 0 24px 60px rgba(0,0,0,0.6)); +} +.modal-header { + margin-bottom: 14px; + justify-content: space-between; + align-items: center; +} +.modal-header h2 { margin: 0; color: var(--cream); } +.modal-header button { + width: 34px; + height: 34px; + display: inline-grid; + place-items: center; + padding: 0; + font-size: 1rem; + line-height: 1; +} + +.toast-region { + position: fixed; + right: 18px; + top: 18px; + z-index: 1500; + display: grid; + gap: 10px; + width: min(380px, calc(100vw - 36px)); + max-height: calc(100vh - 36px); + overflow: auto; +} +.toast { + justify-content: space-between; + align-items: flex-start; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + padding: 12px; + background: rgba(16, 4, 5, 0.98); + box-shadow: var(--shadow-card); +} +.toast-kind-icon { + display: inline-grid; + flex: 0 0 24px; + width: 24px; + height: 24px; + place-items: center; + border: 1px solid currentColor; + border-radius: 50%; + font-weight: 800; +} +.toast p { margin: 4px 0 0; color: var(--cream-mute); } +.toast-pending .toast-kind-icon { color: var(--text-secondary); } +.toast-success { border-color: rgba(143, 176, 138, 0.45); animation: toast-confirm 280ms ease-out both; } +.toast-success .toast-kind-icon { color: var(--signal-ok); } +.toast-error { border-color: rgba(255, 123, 124, 0.45); } +.toast-error .toast-kind-icon { color: var(--coral-hot); } +.toast-hash { margin-top: 6px; overflow-wrap: anywhere; } +@keyframes toast-confirm { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } + +.skeleton { + display: inline-block; + max-width: 100%; + border-radius: var(--r-sm, 3px); + background: linear-gradient(90deg, var(--coral-a04), var(--coral-a20), var(--coral-a04)); + background-size: 220% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + vertical-align: middle; +} +@keyframes skeleton-shimmer { from { background-position: 160% 0; } to { background-position: -60% 0; } } + +.create-pool-type-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 12px; + margin-top: 12px; +} +.create-pool-type { + position: relative; + grid-template-columns: auto minmax(0, 1fr); + align-items: flex-start; + gap: 10px; + min-width: 0; + cursor: pointer; +} +.create-pool-type input { position: absolute; opacity: 0; pointer-events: none; } +.create-pool-type.active { + border-color: var(--line-strong); + background: var(--coral-a08); +} +.create-pool-type.disabled { + opacity: 1; + cursor: not-allowed; + border-color: var(--line-hair); + background: var(--surface-card); +} +.create-pool-type-radio { + width: 14px; + height: 14px; + margin-top: 3px; + border: 1px solid var(--line-soft); + border-radius: 50%; + background: var(--surface-inset); +} +.create-pool-type.active .create-pool-type-radio { + border-color: var(--coral); + box-shadow: inset 0 0 0 3px var(--surface-card); + background: var(--coral); +} +.create-pool-type-copy { + display: grid; + gap: 6px; + min-width: 0; +} +.create-pool-type-copy strong { + color: var(--cream); + font-size: 1rem; + line-height: 1.25; +} +.create-pool-type-copy span, +.create-pool-type-copy small { + min-width: 0; + line-height: 1.45; +} +.create-pool-assets { margin-top: 0; } +.custom-asset-grid { + grid-template-columns: minmax(120px, 0.7fr) minmax(170px, 0.9fr) minmax(260px, 1.7fr) minmax(130px, 0.8fr) minmax(96px, 0.5fr); + align-items: end; + margin-top: 2px; +} + +.price-chart-card { display: grid; gap: 14px; padding: 16px; } +.price-chart-compact { padding: 10px; gap: 8px; box-shadow: none; } +.price-chart-header { align-items: flex-start; } +.price-chart-header h3 { margin: 0; color: var(--cream); } +.segmented-control { + display: inline-flex; + gap: 4px; + padding: 4px; + border: 1px solid var(--line-hair); + border-radius: var(--r-md, 5px); + background: var(--surface-inset); +} +.chart-plot { + position: relative; + min-height: 304px; + padding: 8px 54px 34px 0; + border-radius: var(--r-sm, 3px); + background: var(--surface-inset); +} +.chart-plot-compact { + min-height: 86px; + padding: 0; + background: transparent; +} +.price-chart-svg { + width: 100%; + height: 260px; + overflow: visible; + display: block; +} +.chart-axis line { + stroke: rgba(233, 226, 208, 0.12); + stroke-width: 1; + vector-effect: non-scaling-stroke; +} +.chart-axis .chart-axis-line { + stroke: rgba(233, 226, 208, 0.28); +} +.chart-y-labels, +.chart-x-labels, +.chart-hover-labels { + position: absolute; + inset: 8px 54px 34px 0; + pointer-events: none; +} +.chart-y-labels span, +.chart-x-labels span { + position: absolute; + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.04em; + white-space: nowrap; +} +.chart-y-labels span { + left: calc(100% + 10px); + transform: translateY(-50%); +} +.chart-y-unit { + position: absolute; + left: calc(100% + 10px); + top: -2px; + color: var(--cream); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.chart-x-labels { + inset-block-start: auto; + height: 22px; + bottom: 8px; +} +.chart-x-labels span { + bottom: 0; + transform: translateX(-50%); +} +.chart-point { + fill: var(--surface-inset); + stroke: var(--coral); + stroke-width: 1.6; + vector-effect: non-scaling-stroke; +} +.chart-point-hit { + fill: transparent; + stroke: transparent; + pointer-events: all; +} +.chart-point-group { + outline: none; + pointer-events: all; +} +.chart-hover-label { + position: absolute; + display: grid; + gap: 2px; + min-width: 54px; + padding: 5px 7px; + border: 1px solid rgba(255, 123, 124, 0.34); + border-radius: var(--r-sm, 3px); + background: rgba(16, 18, 24, 0.82); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.22); + opacity: 0; + transform: translate(-50%, calc(-100% - 10px)); + transition: opacity 120ms ease; +} +.chart-hover-label.left { transform: translate(calc(-100% - 8px), -50%); } +.chart-hover-label.right { transform: translate(8px, -50%); } +.chart-hover-label.below { transform: translate(-50%, 10px); } +.chart-hover-label small { + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.chart-hover-label strong { + color: var(--cream); + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; +} +.chart-hover-label.visible { + opacity: 1; +} +.chart-price-readout { + display: grid; + gap: 2px; + justify-items: end; + text-align: right; +} + +.chart-data-summary { + color: var(--cream-mute); + font-size: 0.82rem; +} + +.chart-data-summary table { margin-top: 10px; border-collapse: collapse; } +.chart-data-summary caption { text-align: left; color: var(--cream); margin-bottom: 6px; } +.chart-data-summary th, +.chart-data-summary td { padding: 4px 12px 4px 0; text-align: left; } +.chart-price-readout small { + color: var(--cream-faint); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.chart-price-readout strong { + color: var(--cream); + font-family: var(--font-display, var(--font-heading)); + font-size: 1.25rem; + font-weight: 500; +} +.price-chart-card .spark-fill { + fill: rgba(255, 123, 124, 0.16); +} +.price-chart-card .spark-line { + fill: none; + stroke: var(--coral); + stroke-width: 2; + vector-effect: non-scaling-stroke; +} +.price-chart-compact .price-chart-header h3 { font-size: 0.95rem; } +.price-chart-compact .price-chart-subtitle { font-size: 0.78rem; } +.price-chart-compact .price-chart-svg { height: 86px; } +.candle-up line, +.candle-up rect { + stroke: var(--signal-ok); + fill: rgba(143, 176, 138, 0.68); +} +.candle-down line, +.candle-down rect { + stroke: var(--coral-deep); + fill: rgba(232, 95, 96, 0.68); +} + +.app-footer { display: none; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} + +@media (max-width: 860px) { + .dex-shell { + grid-template-columns: 1fr; + grid-template-rows: auto auto 1fr; + } + + .app-topbar { + display: none; + } + + .topbar-coord { + display: none; + } + + .app-header { + grid-column: 1; + grid-row: 1; + position: sticky; + top: 0; + width: auto; + height: auto; + padding: 10px 12px; + border-right: 0; + border-bottom: 1px solid var(--line-hair); + background: rgba(10, 2, 3, 0.96); + gap: 8px; + z-index: 80; + } + + .sidebar-network { + display: none; + } + + .network-guard-banner { + grid-column: 1; + grid-row: 2; + margin: 12px 12px 0; + } + + .app-main { + grid-column: 1; + grid-row: 3; + padding: 16px 12px 88px; + } + + .page-grid, + .swap-page-grid { + grid-template-columns: 1fr; + width: 100%; + max-width: 560px; + } + .header-inner { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + justify-content: stretch; + } + .mobile-header-account { display: block; min-width: 0; } + .mobile-header-account .wallet-stack { justify-items: end; } + .mobile-header-account .wallet-button { margin: 0; min-height: 44px; } + .brand-lockup { align-items: center; } + .topbar-actions { justify-content: flex-end; width: 100%; } + .pool-list-controls { grid-template-columns: 1fr 1fr; } + .pool-table-header, + .wallet-history-row-head { display: none; } + .pool-row, + .wallet-history-row, + .stats-top-pool-row { grid-template-columns: 1fr; } + .wallet-history-filters { grid-template-columns: 1fr 1fr; } + .pool-assets { grid-template-columns: 1fr; } + .lp-position-metrics, + .portfolio-hero { + display: grid; + } + .portfolio-wallet-chip { + width: fit-content; + max-width: 100%; + } + .form-grid { flex-direction: column; align-items: stretch; } + .asset-amount-card .form-grid { grid-template-columns: minmax(0, 1fr) max-content; } + .mobile-nav-toggle { display: inline-flex; } + .primary-nav { + display: none; + grid-template-columns: 1fr; + gap: 8px; + } + .primary-nav.is-open { display: grid; } + .primary-nav .nav-link { justify-content: flex-start; min-height: 44px; } + .mobile-quick-nav { + position: fixed; + right: 10px; + bottom: max(10px, env(safe-area-inset-bottom)); + left: 10px; + z-index: 70; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 4px; + padding: 6px; + border: 1px solid var(--line-strong); + border-radius: var(--r-md, 5px); + background: rgba(16, 4, 5, 0.96); + box-shadow: var(--shadow-card); + backdrop-filter: blur(12px); + } + .mobile-quick-nav a, + .mobile-quick-nav button { + min-width: 0; + min-height: 44px; + margin: 0; + padding: 8px 4px; + border: 0; + border-radius: var(--r-sm, 3px); + background: transparent; + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.75rem; + text-align: center; + } + .mobile-quick-nav a { display: grid; place-items: center; } + .mobile-quick-nav a.active { background: var(--coral-a12); color: var(--coral-bright); } + .mobile-quick-nav button:disabled { color: var(--text-muted); opacity: 1; } + .transaction-center { bottom: 76px; } +} + +@media (max-width: 640px) { + .token-row-main { grid-template-columns: auto minmax(0, 1fr); } + .token-row-meta { grid-column: 2; text-align: left; } +} + +@media (max-width: 600px) { + .dex-shell { width: 100%; } + .app-header { + padding: 10px; + } + .brand-logo { width: 24px; height: 24px; } + .brand-title img { height: 14px; width: auto; } + .eyebrow { font-size: 0.75rem; } + .topbar-actions { gap: 8px; } + .topbar-actions .status-pill { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + .wallet-stack { justify-items: start; } + .wallet-button, + .icon-button, + .mobile-nav-toggle { padding: 9px 10px; } + .wallet-button, + .mobile-nav-toggle, + .primary-action, + .token-selector-trigger { min-height: 44px; } + .wallet-button.connected { + max-width: 100%; + } + .asset-amount-card .token-selector-trigger { + min-width: 116px; + max-width: 124px; + } + .swap-card-header { flex-wrap: nowrap; align-items: flex-start; } + .swap-card-header > div:first-child { min-width: 0; } + .swap-settings { flex: none; } + .swap-page-grid .swap-card .asset-amount-card .form-grid { + grid-template-columns: 1fr; + } + .swap-page-grid .swap-card .asset-amount-card .token-selector { + position: absolute; + top: 38px; + right: 0; + z-index: 1; + } + .swap-page-grid .swap-card .asset-amount-card .token-amount-row input { padding-right: 132px; } + .swap-page-grid .swap-card .token-amount-actions { flex-wrap: wrap; gap: 6px; } + .swap-page-grid .swap-card .token-amount-actions button { padding-inline: 8px; } + .swap-page-grid .swap-card .token-amount-actions .fiat-hint { + flex: 1 0 100%; + order: 2; + } + .asset-amount-card .token-amount-row input { + font-size: 1.75rem; + } + .nav-link { text-align: center; padding: 10px 8px; } + .settings-panel { + right: 8px; + width: calc(100vw - 24px); + } + .hero-panel, + .panel-page, + .swap-card { padding: 16px; } + .pool-list-controls, + .custom-asset-grid, + .create-pool-type-grid, + .portfolio-total-grid, + .lp-position-metrics { + grid-template-columns: 1fr; + } + .create-pool-page .form-grid, + .create-pool-page .pool-assets { + grid-template-columns: 1fr; + } + dl div, + .quote-details div { + grid-template-columns: 1fr; + gap: 4px; + } + .quote-detail-value { text-align: left; } +} diff --git a/frontend/src/theme/junoTheme.ts b/frontend/src/theme/junoTheme.ts new file mode 100644 index 000000000..e52508ac7 --- /dev/null +++ b/frontend/src/theme/junoTheme.ts @@ -0,0 +1,208 @@ +import type { CSSProperties } from "react"; +import type { ThemeProviderProps } from "@interchain-ui/react"; + +export const junoTokens = { + color: { + canvas: "#0A0203", + canvasElevated: "#100405", + panel: "#230A0C", + panelMuted: "#2E1214", + panelGlass: "rgba(39, 11, 13, 0.78)", + border: "rgba(255, 123, 124, 0.12)", + borderStrong: "rgba(255, 123, 124, 0.35)", + text: "#FFEBD2", + textMuted: "#B69C82", + // Small labels must remain readable over every dark surface (WCAG AA). + textSubtle: "#B69C82", + primary: "#FF7B7C", + primaryStrong: "#E85F60", + primarySoft: "rgba(255, 123, 124, 0.08)", + indigo: "#C6484A", + cyan: "#FF7B7C", + cyanSoft: "rgba(255, 123, 124, 0.08)", + coral: "#FF7B7C", + coralSoft: "rgba(255, 123, 124, 0.12)", + success: "#8FB08A", + warning: "#E9A94F", + }, + space: { + xs: "0.5rem", + sm: "0.75rem", + md: "1rem", + lg: "1.5rem", + xl: "2rem", + "2xl": "3rem", + }, + radius: { + sm: "3px", + md: "5px", + lg: "8px", + xl: "8px", + pill: "999px", + }, + typography: { + fontFamily: "Montserrat, Gotham, 'Helvetica Neue', Arial, sans-serif", + headingTracking: "0", + eyebrowTracking: "0.18em", + }, + shadow: { + panel: "0 1px 0 rgba(255,235,210,0.03), 0 12px 32px rgba(0,0,0,0.5)", + glow: "0 0 0 1px rgba(255,123,124,0.4), 0 0 24px rgba(255,123,124,0.28)", + }, +} as const; + +type ThemeDef = NonNullable[number]; + +export const interchainJunoTheme: ThemeDef = { + name: "juno-dark", + vars: { + colors: { + primary: junoTokens.color.primary, + background: junoTokens.color.canvas, + body: junoTokens.color.canvas, + text: junoTokens.color.text, + textSecondary: junoTokens.color.textMuted, + textMuted: junoTokens.color.textSubtle, + textInverse: junoTokens.color.canvas, + link: junoTokens.color.cyan, + linkHover: junoTokens.color.primary, + cardBg: junoTokens.color.panel, + inputBg: junoTokens.color.canvasElevated, + inputBorder: junoTokens.color.border, + inputBorderFocus: junoTokens.color.primary, + divider: junoTokens.color.border, + accent: junoTokens.color.cyan, + accentText: junoTokens.color.canvas, + textDanger: junoTokens.color.coral, + textWarning: junoTokens.color.warning, + textSuccess: junoTokens.color.success, + primary50: "#F1EDFF", + primary100: "#FFEBD2", + primary200: "#FFB0B1", + primary300: "#FF9698", + primary400: "#FF7B7C", + primary500: "#E85F60", + primary600: "#C6484A", + primary700: "#7A3536", + primary800: "#351416", + primary900: "#270B0D", + purple500: "#FF7B7C", + purple600: "#E85F60", + blue500: "#B69C82", + blue600: "#B69C82", + gray800: "#230A0C", + gray900: "#100405", + }, + font: { + body: junoTokens.typography.fontFamily, + }, + radii: { + md: junoTokens.radius.md, + lg: junoTokens.radius.lg, + xl: junoTokens.radius.xl, + full: junoTokens.radius.pill, + }, + }, +}; + +export const interchainThemeProps = { + defaultTheme: "dark", + themeMode: "dark", + accent: "red", + customTheme: interchainJunoTheme.name, + themeDefs: [interchainJunoTheme], +} satisfies Pick; + +export const junoCssVars = { + "--juno-color-canvas": junoTokens.color.canvas, + "--juno-color-canvas-elevated": junoTokens.color.canvasElevated, + "--juno-color-panel": junoTokens.color.panel, + "--juno-color-panel-muted": junoTokens.color.panelMuted, + "--juno-color-panel-glass": junoTokens.color.panelGlass, + "--juno-color-border": junoTokens.color.border, + "--juno-color-border-strong": junoTokens.color.borderStrong, + "--juno-color-text": junoTokens.color.text, + "--juno-color-text-muted": junoTokens.color.textMuted, + "--juno-color-text-subtle": junoTokens.color.textSubtle, + "--juno-color-primary": junoTokens.color.primary, + "--juno-color-primary-strong": junoTokens.color.primaryStrong, + "--juno-color-primary-soft": junoTokens.color.primarySoft, + "--juno-color-indigo": junoTokens.color.indigo, + "--juno-color-cyan": junoTokens.color.cyan, + "--juno-color-cyan-soft": junoTokens.color.cyanSoft, + "--juno-color-coral": junoTokens.color.coral, + "--juno-color-coral-soft": junoTokens.color.coralSoft, + "--juno-color-success": junoTokens.color.success, + "--juno-color-warning": junoTokens.color.warning, + "--coral": junoTokens.color.primary, + "--coral-bright": "#FF9698", + "--coral-hot": "#FFB0B1", + "--coral-deep": junoTokens.color.primaryStrong, + "--coral-blood": "#C6484A", + "--coral-a04": "rgba(255, 123, 124, 0.04)", + "--coral-a08": "rgba(255, 123, 124, 0.08)", + "--coral-a12": "rgba(255, 123, 124, 0.12)", + "--coral-a20": "rgba(255, 123, 124, 0.20)", + "--coral-a35": "rgba(255, 123, 124, 0.35)", + "--coral-a60": "rgba(255, 123, 124, 0.60)", + "--maroon": "#270B0D", + "--maroon-deep": "#1B0708", + "--void": "#100405", + "--void-pure": "#0A0203", + "--cream": junoTokens.color.text, + "--cream-dim": "#E3CDB2", + "--cream-mute": junoTokens.color.textMuted, + "--cream-faint": junoTokens.color.textSubtle, + "--cream-a06": "rgba(255, 235, 210, 0.06)", + "--cream-a10": "rgba(255, 235, 210, 0.10)", + "--cream-a20": "rgba(255, 235, 210, 0.20)", + "--surface-void": junoTokens.color.canvas, + "--surface-ground": "#270B0D", + "--surface-raised": junoTokens.color.panelMuted, + "--surface-card": junoTokens.color.panel, + "--surface-inset": junoTokens.color.canvasElevated, + "--text-primary": junoTokens.color.text, + "--text-secondary": "#E3CDB2", + "--text-muted": junoTokens.color.textMuted, + "--text-faint": junoTokens.color.textSubtle, + "--text-accent": junoTokens.color.primary, + "--text-on-coral": "#1B0708", + "--line-hair": junoTokens.color.border, + "--line-soft": "rgba(255, 123, 124, 0.20)", + "--line-strong": junoTokens.color.borderStrong, + "--line-cream": "rgba(255, 235, 210, 0.10)", + "--signal-ok": junoTokens.color.success, + "--signal-warn": junoTokens.color.warning, + "--signal-live": junoTokens.color.primary, + "--focus-ring": "rgba(255, 123, 124, 0.60)", + "--juno-radius-sm": junoTokens.radius.sm, + "--juno-radius-md": junoTokens.radius.md, + "--juno-radius-lg": junoTokens.radius.lg, + "--juno-radius-xl": junoTokens.radius.xl, + "--juno-radius-pill": junoTokens.radius.pill, + "--juno-space-xs": junoTokens.space.xs, + "--juno-space-sm": junoTokens.space.sm, + "--juno-space-md": junoTokens.space.md, + "--juno-space-lg": junoTokens.space.lg, + "--juno-space-xl": junoTokens.space.xl, + "--juno-shadow-panel": junoTokens.shadow.panel, + "--juno-shadow-glow": junoTokens.shadow.glow, + // Short-name aliases consumed by styles/theme.css. Without these the + // referenced shadow/glow/radius tokens resolve to nothing and cards + // render flat (no elevation). Sharp radii per the "diagrammatic, not + // friendly-rounded" rule. + "--r-sm": junoTokens.radius.sm, + "--r-md": junoTokens.radius.md, + "--r-lg": junoTokens.radius.lg, + "--r-pill": junoTokens.radius.pill, + "--shadow-card": "0 1px 0 rgba(255,235,210,0.03), 0 18px 40px rgba(0,0,0,0.55)", + "--shadow-pop": "0 24px 60px rgba(0,0,0,0.62)", + "--glow-soft": "0 0 16px rgba(255,123,124,0.22)", + "--glow-coral": "0 0 0 1px rgba(255,123,124,0.35), 0 0 22px rgba(255,123,124,0.30)", + "--juno-font-body": junoTokens.typography.fontFamily, + "--font-display": junoTokens.typography.fontFamily, + "--font-body": junoTokens.typography.fontFamily, + "--font-mono": "'Space Mono', 'IBM Plex Mono', ui-monospace, monospace", + "--juno-heading-tracking": junoTokens.typography.headingTracking, + "--juno-eyebrow-tracking": junoTokens.typography.eyebrowTracking, +} as CSSProperties; diff --git a/frontend/src/tx/TxHistoryContext.test.tsx b/frontend/src/tx/TxHistoryContext.test.tsx new file mode 100644 index 000000000..6207de32c --- /dev/null +++ b/frontend/src/tx/TxHistoryContext.test.tsx @@ -0,0 +1,32 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { TransactionCenter } from "../components/tx/TransactionCenter"; +import { TX_HISTORY_STORAGE_KEY, TxHistoryProvider, useTxHistory } from "./TxHistoryContext"; + +function AddRecord() { + const { upsert } = useTxHistory(); + return ; +} + +describe("TxHistoryProvider", () => { + beforeEach(() => window.localStorage.clear()); + + it("persists recent transactions and restores their explorer path after remount", () => { + const first = render(); + fireEvent.click(screen.getByRole("button", { name: /add transaction/i })); + expect(JSON.parse(window.localStorage.getItem(TX_HISTORY_STORAGE_KEY) ?? "[]")).toHaveLength(1); + first.unmount(); + + render(); + expect(screen.getByText("1 JUNO swapped")).toBeTruthy(); + expect(screen.getByRole("link", { name: /view in explorer/i }).getAttribute("href")).toContain("/tx/ABC"); + }); + + it("restores an in-flight status after its originating route unmounts", () => { + window.localStorage.setItem(TX_HISTORY_STORAGE_KEY, JSON.stringify([{ id: "pending", title: "Add liquidity", status: "awaiting-signature", description: "Confirm in wallet", updatedAt: 20 }])); + render(); + expect(screen.getByText("Add liquidity")).toBeTruthy(); + expect(screen.getByText("awaiting signature")).toBeTruthy(); + expect(screen.queryByRole("button", { name: /dismiss/i })).toBeNull(); + }); +}); diff --git a/frontend/src/tx/TxHistoryContext.tsx b/frontend/src/tx/TxHistoryContext.tsx new file mode 100644 index 000000000..78f3db23b --- /dev/null +++ b/frontend/src/tx/TxHistoryContext.tsx @@ -0,0 +1,55 @@ +import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; +import type { TxLifecycleStatus } from "./useTxRunner"; + +export const TX_HISTORY_STORAGE_KEY = "juno-dex.transaction-history"; + +export type PersistedTxRecord = { + id: string; + title: string; + status: Exclude; + description?: string; + txHash?: string; + updatedAt: number; +}; + +type TxHistoryContextValue = { + records: PersistedTxRecord[]; + upsert: (record: PersistedTxRecord) => void; + dismiss: (id: string) => void; + centerOpen: boolean; + setCenterOpen: (open: boolean) => void; +}; + +const TxHistoryContext = createContext(undefined); + +function readStoredRecords(): PersistedTxRecord[] { + if (typeof window === "undefined") return []; + try { + const parsed = JSON.parse(window.localStorage.getItem(TX_HISTORY_STORAGE_KEY) ?? "[]") as PersistedTxRecord[]; + return Array.isArray(parsed) ? parsed.filter((record) => record && typeof record.id === "string" && typeof record.updatedAt === "number").slice(0, 20) : []; + } catch { + return []; + } +} + +export function TxHistoryProvider({ children }: { children: ReactNode }) { + const [records, setRecords] = useState(readStoredRecords); + const [centerOpen, setCenterOpen] = useState(false); + const normalizeAndStore = useCallback((next: PersistedTxRecord[]) => { + const limited = [...next].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 20); + if (typeof window !== "undefined") window.localStorage.setItem(TX_HISTORY_STORAGE_KEY, JSON.stringify(limited)); + return limited; + }, []); + const upsert = useCallback((record: PersistedTxRecord) => { + setRecords((current) => normalizeAndStore([record, ...current.filter((candidate) => candidate.id !== record.id)])); + }, [normalizeAndStore]); + const dismiss = useCallback((id: string) => setRecords((current) => normalizeAndStore(current.filter((record) => record.id !== id))), [normalizeAndStore]); + const value = useMemo(() => ({ records, upsert, dismiss, centerOpen, setCenterOpen }), [centerOpen, dismiss, records, upsert]); + return {children}; +} + +const noHistory: TxHistoryContextValue = { records: [], upsert: () => undefined, dismiss: () => undefined, centerOpen: false, setCenterOpen: () => undefined }; + +export function useTxHistory() { + return useContext(TxHistoryContext) ?? noHistory; +} diff --git a/frontend/src/tx/errors.test.ts b/frontend/src/tx/errors.test.ts new file mode 100644 index 000000000..0d1f471d5 --- /dev/null +++ b/frontend/src/tx/errors.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { decodeTxError } from "./errors"; + +describe("decodeTxError", () => { + it("maps max spread errors to slippage guidance", () => { + const decoded = decodeTxError(new Error("execute wasm contract failed: Generic error: Max spread assertion")); + expect(decoded.kind).toBe("max-spread"); + expect(decoded.title).toMatch(/price moved/i); + expect(decoded.retryable).toBe(true); + }); + + it("maps insufficient funds to balance guidance", () => { + const decoded = decodeTxError("insufficient funds: spendable balance 10ujuno is smaller than 100ujuno"); + expect(decoded.kind).toBe("insufficient-funds"); + expect(decoded.message).toMatch(/balance/i); + expect(decoded.retryable).toBe(false); + }); + + it("maps wallet rejection to retryable rejected copy", () => { + const decoded = decodeTxError({ message: "Request rejected by user" }); + expect(decoded.kind).toBe("user-rejected"); + expect(decoded.title).toMatch(/rejected/i); + expect(decoded.retryable).toBe(true); + }); + + it("keeps unknown raw detail visible", () => { + const decoded = decodeTxError("codespace 5: mysterious module error"); + expect(decoded.kind).toBe("unknown"); + expect(decoded.message).toContain("codespace 5: mysterious module error"); + expect(decoded.raw).toBe("codespace 5: mysterious module error"); + }); +}); diff --git a/frontend/src/tx/errors.ts b/frontend/src/tx/errors.ts new file mode 100644 index 000000000..0cbb9e3d3 --- /dev/null +++ b/frontend/src/tx/errors.ts @@ -0,0 +1,93 @@ +export type DecodedTxErrorKind = + | "max-spread" + | "insufficient-funds" + | "slippage" + | "user-rejected" + | "timeout" + | "unknown"; + +export type DecodedTxError = { + kind: DecodedTxErrorKind; + title: string; + message: string; + raw: string; + retryable: boolean; +}; + +function rawErrorDetail(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === "string") return error; + if (typeof error === "object" && error !== null) { + const maybeMessage = "message" in error ? error.message : undefined; + if (typeof maybeMessage === "string") return maybeMessage; + try { + return JSON.stringify(error); + } catch { + return String(error); + } + } + return String(error); +} + +export function decodeTxError(error: unknown): DecodedTxError { + const raw = rawErrorDetail(error); + const normalized = raw.toLowerCase(); + + if (/user (denied|rejected|reject)|request rejected|signature request rejected|declined|cancelled|canceled/.test(normalized)) { + return { + kind: "user-rejected", + title: "Transaction rejected", + message: "Your wallet rejected the signature request. Review the transaction and try again when you are ready.", + raw, + retryable: true, + }; + } + + if (/max[ _-]?spread|assertion failed.*spread|spread limit|belief price|maximum spread/.test(normalized)) { + return { + kind: "max-spread", + title: "Price moved beyond slippage", + message: "The pool price moved outside your allowed max spread. Refresh the quote or increase slippage before retrying.", + raw, + retryable: true, + }; + } + + if (/insufficient (funds|fee|balance)|spendable balance|not enough|cannot subtract|overflow: cannot subtract/.test(normalized)) { + return { + kind: "insufficient-funds", + title: "Insufficient funds", + message: "Your wallet does not have enough balance to cover the amount and network fees.", + raw, + retryable: false, + }; + } + + if (/slippage|minimum receive|minimum amount|less than minimum|belief.*price|tolerance/.test(normalized)) { + return { + kind: "slippage", + title: "Slippage tolerance exceeded", + message: "The received amount would be below your slippage tolerance. Refresh the quote or adjust slippage before retrying.", + raw, + retryable: true, + }; + } + + if (/timeout|timed out|not found after broadcast/.test(normalized)) { + return { + kind: "timeout", + title: "Transaction status timed out", + message: "The transaction was broadcast but indexing took too long. Check recent account activity before preparing another transaction.", + raw, + retryable: true, + }; + } + + return { + kind: "unknown", + title: "Transaction failed", + message: `The chain or wallet returned an unexpected error: ${raw}`, + raw, + retryable: true, + }; +} diff --git a/frontend/src/tx/useTxRunner.test.tsx b/frontend/src/tx/useTxRunner.test.tsx new file mode 100644 index 000000000..af77027b0 --- /dev/null +++ b/frontend/src/tx/useTxRunner.test.tsx @@ -0,0 +1,120 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { QueryClient } from "@tanstack/react-query"; +import { ToastProvider } from "../components/common"; +import { TxStatusDialog } from "../components/tx/TxStatusDialog"; +import { TxHistoryProvider } from "./TxHistoryContext"; +import { walletBalancesQueryKey, type WalletBalance } from "../queries/useWalletBalances"; +import { applyConfirmedExactBalanceDeltas, useTxRunner } from "./useTxRunner"; + +function RunnerHarness({ outcome }: { outcome: "confirmed" | "timeout" | "rejected" }) { + const runner = useTxRunner(); + const run = () => { + void runner.runTx({ + title: "Test swap", + variables: {}, + broadcast: async () => { + if (outcome === "timeout") throw new Error("transaction not found after broadcast timeout"); + if (outcome === "rejected") throw new Error("User rejected the signature request"); + return { transactionHash: "HASH123" }; + }, + }).catch(() => undefined); + }; + return <>; +} + +function renderHarness(outcome: "confirmed" | "timeout" | "rejected") { + return render(); +} + +function DuplicateHarness({ broadcast, onSuccess }: { broadcast: () => Promise<{ transactionHash: string }>; onSuccess?: () => Promise }) { + const runner = useTxRunner(); + const run = () => { void runner.runTx({ title: "Bounded action", variables: {}, broadcast, onSuccess }).catch(() => undefined); }; + return <>; +} + +describe("useTxRunner lifecycle", () => { + it("ends confirmed transactions with a durable explorer link and refresh action", async () => { + renderHarness("confirmed"); + fireEvent.click(screen.getByRole("button", { name: "Run" })); + expect(await screen.findByText("Transaction confirmed")).toBeTruthy(); + expect(screen.getAllByRole("link", { name: /view transaction in explorer/i })[0]?.getAttribute("href")).toContain("/tx/HASH123"); + expect(screen.getByRole("button", { name: /refresh balances and data/i })).toBeTruthy(); + }); + + it("does not offer blind rebroadcast after an ambiguous timeout", async () => { + renderHarness("timeout"); + fireEvent.click(screen.getByRole("button", { name: "Run" })); + await waitFor(() => expect(screen.getByText("Confirmation timed out")).toBeTruthy()); + expect(screen.queryByRole("button", { name: /retry transaction/i })).toBeNull(); + expect(screen.getAllByText(/check recent account activity/i).length).toBeGreaterThan(0); + }); + + it("allows an explicitly rejected wallet request to be prepared again", async () => { + renderHarness("rejected"); + fireEvent.click(screen.getByRole("button", { name: "Run" })); + await waitFor(() => expect(screen.getByText("Rejected in wallet")).toBeTruthy()); + expect(screen.getByRole("button", { name: /retry transaction/i })).toBeTruthy(); + }); + + it("deduplicates rapid confirmation clicks while a broadcast is in flight", async () => { + let resolveBroadcast!: (result: { transactionHash: string }) => void; + const broadcast = vi.fn(() => new Promise<{ transactionHash: string }>((resolve) => { resolveBroadcast = resolve; })); + render(); + + const button = screen.getByRole("button", { name: /run bounded action/i }); + fireEvent.click(button); + fireEvent.click(button); + await waitFor(() => expect(broadcast).toHaveBeenCalledTimes(1)); + resolveBroadcast({ transactionHash: "ONE_HASH" }); + await waitFor(() => expect(screen.getByText("Transaction confirmed")).toBeTruthy()); + }); + + it("prevents rebroadcast while confirmed data is still reconciling", async () => { + let finishIndexing!: () => void; + const onSuccess = vi.fn(() => new Promise((resolve) => { finishIndexing = resolve; })); + const broadcast = vi.fn().mockResolvedValue({ transactionHash: "INDEX_HASH" }); + render(); + + const button = screen.getByRole("button", { name: /run bounded action/i }); + fireEvent.click(button); + await waitFor(() => expect(screen.getByText("Transaction confirmed")).toBeTruthy()); + fireEvent.click(button); + expect(broadcast).toHaveBeenCalledTimes(1); + finishIndexing(); + }); +}); + +describe("confirmed exact balance reconciliation", () => { + const balance = (denom: string, amount: string): WalletBalance => ({ denom, amount, symbol: denom, decimals: 6, source: "registry", isKnownDenom: true }); + + it("updates only existing cached denoms and combines exact deltas", () => { + const queryClient = new QueryClient(); + const key = walletBalancesQueryKey("juno1wallet"); + queryClient.setQueryData(key, [balance("ibc/usdc", "1000"), balance("ujuno", "5000")]); + + applyConfirmedExactBalanceDeltas(queryClient, "juno1wallet", [ + { denom: "ibc/usdc", amount: "-200" }, + { denom: "ibc/usdc", amount: "50" }, + { denom: "unknown", amount: "999" }, + ]); + + expect(queryClient.getQueryData(key)?.map(({ denom, amount }) => ({ denom, amount }))).toEqual([ + { denom: "ibc/usdc", amount: "850" }, + { denom: "ujuno", amount: "5000" }, + ]); + }); + + it("never creates a negative display balance and ignores malformed deltas", () => { + const queryClient = new QueryClient(); + const key = walletBalancesQueryKey("juno1wallet"); + queryClient.setQueryData(key, [balance("factory/token", "10")]); + + applyConfirmedExactBalanceDeltas(queryClient, "juno1wallet", [ + { denom: "factory/token", amount: "-50" }, + { denom: "factory/token", amount: "1.5" }, + ]); + + expect(queryClient.getQueryData(key)?.[0].amount).toBe("0"); + }); +}); diff --git a/frontend/src/tx/useTxRunner.tsx b/frontend/src/tx/useTxRunner.tsx new file mode 100644 index 000000000..39a0db502 --- /dev/null +++ b/frontend/src/tx/useTxRunner.tsx @@ -0,0 +1,182 @@ +import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; +import type { QueryClient, QueryKey } from "@tanstack/react-query"; +import type { RegistryPool } from "../config/registry"; +import { dexRegistry } from "../config/registry"; +import { useToast } from "../components/common"; +import { walletBalancesQueryKey } from "../queries/useWalletBalances"; +import type { WalletBalance } from "../queries/useWalletBalances"; +import { decodeTxError, type DecodedTxError } from "./errors"; +import { useTxHistory } from "./TxHistoryContext"; + +export type TxLifecycleStatus = "idle" | "preparing" | "awaiting-signature" | "submitted" | "confirmed" | "failed" | "rejected" | "timed-out"; + +export type TxResult = { + transactionHash: string; +}; + +export type TxLifecycleState = { + status: TxLifecycleStatus; + label: string; + description?: string; + result?: TxResult; + error?: DecodedTxError; + retry?: () => void | Promise; + refresh?: () => void | Promise; + actionLabel?: string; +}; + +export type RunTxOptions = { + title: string; + pendingMessage?: string; + successMessage?: (result: TxResult, variables: T) => string; + broadcast: (variables: T) => Promise; + variables: T; + onSuccess?: (result: TxResult, variables: T) => Promise | unknown; + retry?: () => void | Promise; +}; + +const statusLabels: Record = { + idle: "Ready", + preparing: "Preparing transaction", + "awaiting-signature": "Awaiting wallet signature", + submitted: "Submitted to Juno", + confirmed: "Transaction confirmed", + failed: "Transaction failed", + rejected: "Rejected in wallet", + "timed-out": "Confirmation timed out", +}; + +function statusFromError(error: DecodedTxError): Extract { + if (error.kind === "user-rejected") return "rejected"; + if (error.kind === "timeout") return "timed-out"; + return "failed"; +} + +export function invalidateDexTxQueries(queryClient: QueryClient, sender: string | undefined, pool?: RegistryPool) { + const invalidations: Promise[] = []; + if (sender) invalidations.push(queryClient.invalidateQueries({ queryKey: walletBalancesQueryKey(sender) })); + invalidations.push(queryClient.invalidateQueries({ queryKey: ["swap-route-quote"] })); + invalidations.push(queryClient.invalidateQueries({ queryKey: pool ? ["pool", pool.pair] : ["pool"] })); + return Promise.all(invalidations); +} + +export type ExactBalanceDelta = { denom: string; amount: string }; + +/** + * Reconciles only confirmed, protocol-exact deltas in an existing balance cache. + * Callers must not use this for JUNO spends (gas is not known here), estimated + * swap receipts, LP mint estimates, rewards, or any other variable outcome. + */ +export function applyConfirmedExactBalanceDeltas(queryClient: QueryClient, sender: string | undefined, deltas: readonly ExactBalanceDelta[]) { + if (!sender || deltas.length === 0) return; + queryClient.setQueryData(walletBalancesQueryKey(sender), (current) => { + if (!current) return current; + const byDenom = new Map(); + for (const delta of deltas) { + if (!/^\-?\d+$/.test(delta.amount)) continue; + byDenom.set(delta.denom, (byDenom.get(delta.denom) ?? 0n) + BigInt(delta.amount)); + } + return current.map((balance) => { + const delta = byDenom.get(balance.denom); + if (delta === undefined || !/^\d+$/.test(balance.amount)) return balance; + const next = BigInt(balance.amount) + delta; + return { ...balance, amount: (next < 0n ? 0n : next).toString() }; + }); + }); +} + +export function TxHashLink({ txHash }: { txHash: string }) { + return {txHash} — view transaction in explorer; +} + +export function useTxRunner() { + const toast = useToast(); + const history = useTxHistory(); + const [state, setState] = useState({ status: "idle", label: statusLabels.idle }); + const inFlightRef = useRef | undefined>(undefined); + + const reset = useCallback(() => setState({ status: "idle", label: statusLabels.idle }), []); + + const runTx = useCallback((options: RunTxOptions): Promise => { + // A second click while the wallet request is open must observe the same + // transaction instead of broadcasting a duplicate irreversible action. + if (inFlightRef.current) return inFlightRef.current; + const task = (async () => { + const txId = `${Date.now()}-${Math.random().toString(16).slice(2)}`; + const retry = options.retry ?? (() => { void runTx(options); }); + setState({ + status: "preparing", + label: statusLabels.preparing, + description: "Building the transaction request and checking the connected account.", + actionLabel: options.title, + }); + history.upsert({ id: txId, title: options.title, status: "preparing", description: "Building the transaction request.", updatedAt: Date.now() }); + await Promise.resolve(); + setState({ + status: "awaiting-signature", + label: statusLabels["awaiting-signature"], + description: options.pendingMessage ?? "Confirm this transaction in your wallet, then wait for it to be broadcast.", + actionLabel: options.title, + retry, + }); + history.upsert({ id: txId, title: options.title, status: "awaiting-signature", description: options.pendingMessage ?? "Confirm in wallet.", updatedAt: Date.now() }); + const pendingToastId = toast.pending({ + title: options.title, + message: options.pendingMessage ?? "Waiting for wallet signature and Juno broadcast…", + }); + + try { + const result = await options.broadcast(options.variables); + setState({ + status: "confirmed", + label: statusLabels.confirmed, + description: options.successMessage?.(result, options.variables) ?? "Juno accepted and indexed the transaction.", + result, + actionLabel: options.title, + refresh: async () => { await options.onSuccess?.(result, options.variables); }, + }); + const successDescription = options.successMessage?.(result, options.variables) ?? "Juno accepted and indexed the transaction."; + history.upsert({ id: txId, title: options.title, status: "confirmed", description: successDescription, txHash: result.transactionHash, updatedAt: Date.now() }); + toast.dismiss(pendingToastId); + toast.success({ + title: `${options.title} succeeded`, + message: options.successMessage?.(result, options.variables) ?? "Transaction indexed successfully.", + txHash: , + }); + await options.onSuccess?.(result, options.variables); + return result; + } catch (caught) { + const decoded = decodeTxError(caught); + const status = statusFromError(decoded); + setState({ + status, + label: statusLabels[status], + description: decoded.message, + error: decoded, + actionLabel: options.title, + retry: decoded.retryable && status !== "timed-out" ? retry : undefined, + }); + history.upsert({ id: txId, title: options.title, status, description: decoded.message, updatedAt: Date.now() }); + toast.dismiss(pendingToastId); + toast.error({ + title: decoded.title, + message: decoded.message, + }); + throw caught; + } + })(); + inFlightRef.current = task; + void task.finally(() => { + if (inFlightRef.current === task) inFlightRef.current = undefined; + }).catch(() => undefined); + return task; + }, [history, toast]); + + return useMemo(() => ({ state, runTx, reset }), [reset, runTx, state]); +} + +export function txLifecycleLabel(status: TxLifecycleStatus) { + return statusLabels[status]; +} + +export type TxStatusLink = ReactNode; diff --git a/frontend/src/wallet/CosmosKitProvider.tsx b/frontend/src/wallet/CosmosKitProvider.tsx new file mode 100644 index 000000000..33d634491 --- /dev/null +++ b/frontend/src/wallet/CosmosKitProvider.tsx @@ -0,0 +1,59 @@ +import { ChainProvider } from "@cosmos-kit/react"; +import { wallets as keplrWallets } from "@cosmos-kit/keplr"; +import { GasPrice } from "@cosmjs/stargate"; +import type { ReactNode } from "react"; +import { JUNO_CHAIN_INFO } from "../config/chains"; +import { junoAssetList, junoChain } from "../config/cosmosKit"; +import { dexRegistry } from "../config/registry"; +import { interchainJunoTheme } from "../theme/junoTheme"; + +const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID as string | undefined; +type ChainProviderProps = Parameters[0]; + +const walletconnectOptions: ChainProviderProps["walletConnectOptions"] = walletConnectProjectId + ? { signClient: { projectId: walletConnectProjectId } } + : undefined; + +const allWallets = [ + ...keplrWallets, +]; + +const wallets = walletconnectOptions + ? allWallets + : allWallets.filter((wallet) => wallet.walletInfo.mode !== "wallet-connect"); + +export function CosmosKitProvider({ children }: { children: ReactNode }) { + return ( + ({ + gasPrice: GasPrice.fromString("0.075ujuno"), + }), + } as never} + > + {children} + + ); +} diff --git a/frontend/src/wallet/WalletContext.tsx b/frontend/src/wallet/WalletContext.tsx new file mode 100644 index 000000000..9da1c78a9 --- /dev/null +++ b/frontend/src/wallet/WalletContext.tsx @@ -0,0 +1,129 @@ +import { createContext, useContext, useMemo, type ReactNode } from "react"; +import { useChain } from "@cosmos-kit/react"; +import { JUNO_CHAIN_INFO } from "../config/chains"; +import { COSMOS_KIT_CHAIN_NAME } from "../config/cosmosKit"; +import { createE2ESigningClient, E2E_WALLET_ADDRESS, isE2EMode } from "../e2e/mocks"; +import type { NetworkGuardState, WalletState } from "./types"; + +type WalletContextValue = { + wallet: WalletState; + network: NetworkGuardState; + connect: () => Promise; + disconnect: () => Promise; + openView: () => void; + switchToJuno: () => Promise; +}; + +const WalletContext = createContext(undefined); + +export function WalletProvider({ children }: { children: ReactNode }) { + if (isE2EMode()) { + return {children}; + } + const cosmosWallet = useCosmosKitWallet(); + return {children}; +} + +function createE2EWalletContext(): WalletContextValue { + const wallet: WalletState = { + status: "connected", + address: E2E_WALLET_ADDRESS, + name: "Playwright Wallet", + chainId: JUNO_CHAIN_INFO.chainId, + signer: async () => createE2ESigningClient() as never, + }; + return { + wallet, + network: { + expectedChainId: JUNO_CHAIN_INFO.chainId, + connectedChainId: JUNO_CHAIN_INFO.chainId, + isWalletConnected: true, + isRecovering: false, + isWrongNetwork: false, + isJunoReady: true, + }, + connect: async () => undefined, + disconnect: async () => undefined, + openView: () => undefined, + switchToJuno: async () => undefined, + }; +} + +export function useCosmosKitWallet(): WalletContextValue { + const chain = useChain(COSMOS_KIT_CHAIN_NAME); + const expectedChainId = JUNO_CHAIN_INFO.chainId; + const connectedChainId = chain.chain?.chain_id; + + const wallet = useMemo(() => { + if (chain.isWalletConnected && chain.address) { + return { + status: "connected", + address: chain.address, + name: chain.username ?? chain.wallet?.prettyName, + chainId: connectedChainId, + signer: chain.getOfflineSigner(), + }; + } + + if (chain.isWalletConnecting) return { status: "connecting" }; + + if (chain.isWalletRejected || chain.isWalletNotExist || chain.isWalletError) { + return { + status: "error", + error: chain.message ?? "Wallet connection failed. Read-only mode remains available.", + }; + } + + return { status: "idle" }; + }, [chain, connectedChainId]); + + const network = useMemo(() => { + const isWalletConnected = wallet.status === "connected"; + const isWrongNetwork = isWalletConnected && connectedChainId !== expectedChainId; + const needsEnable = wallet.status === "error" && /enable|chain|not exist|not found|reject/i.test(wallet.error ?? chain.message ?? ""); + + return { + expectedChainId, + connectedChainId, + isWalletConnected, + isRecovering: chain.isWalletConnecting, + isWrongNetwork, + isJunoReady: !isWalletConnected || (!isWrongNetwork && wallet.status === "connected"), + message: isWrongNetwork + ? `Wallet is connected to ${connectedChainId ?? "an unknown chain"}. Switch to Juno (${expectedChainId}) before broadcasting transactions.` + : needsEnable + ? `Juno (${expectedChainId}) is not enabled in this wallet yet. Switch to Juno to continue.` + : undefined, + }; + }, [chain.isWalletConnecting, chain.message, connectedChainId, expectedChainId, wallet]); + + const switchToJuno = async () => { + await chain.enable(); + if (!chain.isWalletConnected) await chain.connect(); + }; + + const value = useMemo( + () => ({ + wallet, + network, + connect: async () => chain.openView(), + disconnect: async () => chain.disconnect(), + openView: chain.openView, + switchToJuno, + }), + [chain, network, wallet], + ); + + return value; +} + +export function useWallet() { + const context = useContext(WalletContext); + if (!context) throw new Error("useWallet must be used within WalletProvider"); + return context; +} + +export function useNetworkGuard() { + const { network, switchToJuno } = useWallet(); + return { network, switchToJuno }; +} diff --git a/frontend/src/wallet/types.ts b/frontend/src/wallet/types.ts new file mode 100644 index 000000000..45c7d6a74 --- /dev/null +++ b/frontend/src/wallet/types.ts @@ -0,0 +1,20 @@ +import type { SigningClientSource } from "../lib/cosmjs/clients"; + +export type WalletState = { + status: "idle" | "connecting" | "connected" | "error"; + address?: string; + name?: string; + error?: string; + chainId?: string; + signer?: SigningClientSource; +}; + +export type NetworkGuardState = { + expectedChainId: "juno-1"; + connectedChainId?: string; + isWalletConnected: boolean; + isRecovering: boolean; + isWrongNetwork: boolean; + isJunoReady: boolean; + message?: string; +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 000000000..468d6c342 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src"], + "references": [] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 000000000..22a28e15f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + globals: true, + exclude: ["e2e/**", "node_modules/**", "dist/**"], + }, +}); diff --git a/indexer/.dockerignore b/indexer/.dockerignore new file mode 100644 index 000000000..72f583ea5 --- /dev/null +++ b/indexer/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.env +npm-debug.log diff --git a/indexer/.env.example b/indexer/.env.example new file mode 100644 index 000000000..4fd77c7af --- /dev/null +++ b/indexer/.env.example @@ -0,0 +1,35 @@ +DATABASE_URL=postgres://postgres:postgres@localhost:5432/astroport_indexer +JUNO_RPC_URL=https://juno-rpc.publicnode.com:443 +JUNO_REST_URL=https://juno-rest.publicnode.com +JUNO_WS_URL=wss://juno-rpc.publicnode.com:443/websocket +CHAIN_ID=juno-1 +FACTORY_ADDRESS=juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca +ROUTER_ADDRESS=juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s +INCENTIVES_ADDRESS=juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598 +ORACLE_ADDRESS=juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p +NATIVE_COIN_REGISTRY_ADDRESS=juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2 +START_HEIGHT=39381297 +CONFIRMATION_DEPTH=2 +POLL_INTERVAL_MS=5000 +BATCH_SIZE=20 +DRY_RUN=false +INDEXER_MODE=realtime +RANGE_SIZE=5000 +FETCH_WINDOW_SIZE=250 +FETCH_CONCURRENCY=32 +REALTIME_FETCH_CONCURRENCY=8 +RPC_TIMEOUT_MS=10000 +RPC_MAX_RETRIES=5 +INGEST_CANDLES_INLINE=true +INGEST_RESERVE_SNAPSHOTS_INLINE=true +INGEST_AGGREGATES_INLINE=false +INGEST_BULK_STAGING_ENABLED=false +READ_MODEL_REFRESH_INTERVAL_MS=15000 +API_PORT=8787 +PRICE_PROVIDER_BASE_URL= +PRICE_PROVIDER_API_KEY= +PRICE_PROVIDER_NAME=provider +PRICE_CACHE_TTL_MS=300000 +PRICE_STALE_AFTER_MS=1800000 +PRICE_ALLOW_STALE=true +PRICE_DEV_MOCKS=false diff --git a/indexer/Dockerfile b/indexer/Dockerfile new file mode 100644 index 000000000..1f8d6e342 --- /dev/null +++ b/indexer/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci + +FROM deps AS build +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=build /app/dist ./dist +COPY migrations ./migrations +CMD ["node", "dist/src/index.js"] diff --git a/indexer/README.md b/indexer/README.md new file mode 100644 index 000000000..7ccc84ae5 --- /dev/null +++ b/indexer/README.md @@ -0,0 +1,190 @@ +# Astroport Juno Indexer + +Production indexer/API foundation for Juno Astroport pool metrics, history, LP positions, candles, and frontend-facing market data. + +## Stack decision + +This service uses a small TypeScript/Node block poller over Juno Tendermint RPC/REST instead of SubQuery. The repo already ships a TypeScript frontend, and a lightweight poller keeps the foundational service easy to run locally, test without chain or DB infrastructure, and evolve into the API/metrics work in issues #39-#42. The ingestion core is split into pure event-normalization helpers plus a Postgres writer so unit tests do not require live infra. + +## What is included + +- Postgres migration for: + - resumable cursors and block processing ledger + - pools and pool state snapshots + - swaps and liquidity events + - incentive events + - LP positions/balances + - token prices and OHLC candles +- Idempotent transaction/event shape based on `(tx_hash, msg_index, event_index, action)` uniqueness. +- Reorg-aware block ledger fields (`height`, `block_hash`, `parent_hash`) and configurable confirmation depth. +- Juno RPC/REST/WebSocket configuration placeholders for poll/backfill/live modes. +- Height-safe LCD pool-state queries after swap/provide/withdraw events, persisted as reserve snapshots keyed by `(pool_id, height, source)`. +- Unit-tested event normalization for factory, pair, and incentives events. +- Swap-derived pool OHLC candle writes for `5m`, `1h`, and `1d` intervals plus a replayable candle backfill command. +- HTTP API routes for `/health`, `/ready`, `/openapi.json`, `/stats`, `/prices`, `/pools`, pool candles, pool positions, wallet positions, and wallet history. +- Pool API responses expose latest persisted reserves and total LP share from `pool_state_snapshots` when snapshot rows exist. +- Optional JUNO-denominated value fields alongside honest nullable USD fields. + +## Configuration + +Copy `.env.example` to `.env` or export variables: + +| Variable | Default | Description | +| --- | --- | --- | +| `DATABASE_URL` | `postgres://postgres:postgres@localhost:5432/astroport_indexer` | Postgres connection string. Use host-managed secrets in production. | +| `JUNO_RPC_URL` | `https://juno-rpc.publicnode.com:443` | Tendermint RPC endpoint. | +| `JUNO_REST_URL` | `https://juno-rest.publicnode.com` | Cosmos REST endpoint used for height-pinned pair pool-state smart queries. | +| `JUNO_WS_URL` | derived from RPC | WebSocket endpoint for future tailing. | +| `CHAIN_ID` | `juno-1` | Expected chain id. | +| `FACTORY_ADDRESS` | deployed Juno v1 factory | Astroport factory contract. | +| `ROUTER_ADDRESS` | deployed Juno v1 router | Router contract, retained for downstream API context. | +| `INCENTIVES_ADDRESS` | deployed Juno v1 incentives | Incentives contract to watch. | +| `ORACLE_ADDRESS` | deployed Juno v1 oracle | Oracle contract for price/candle work. | +| `NATIVE_COIN_REGISTRY_ADDRESS` | deployed Juno v1 native registry | Native registry contract. | +| `START_HEIGHT` | `39381297` | Juno v1 factory deployment height. Override only for intentional archive/full-chain replays. | +| `CONFIRMATION_DEPTH` | `2` | Blocks to lag chain head for reorg safety. | +| `POLL_INTERVAL_MS` | `5000` | Poll cadence. | +| `BATCH_SIZE` | `20` | Max blocks per polling loop. | +| `DRY_RUN` | `false` | If true, normalizes and logs without DB writes. | +| `INDEXER_MODE` | `realtime` | Runtime mode selector. Use `realtime` for the live poller path or `catchup` for bounded/backfill catch-up orchestration. | +| `RANGE_SIZE` | `5000` | Catch-up process range size for future high-throughput orchestration. Must be at least `1`. | +| `FETCH_WINDOW_SIZE` | `250` | Maximum block fetch scheduling window for future high-throughput RPC fetchers. Must be at least `1`. | +| `FETCH_CONCURRENCY` | `32` | Catch-up RPC fetch concurrency for future high-throughput fetchers. Must be at least `1` and no greater than `FETCH_WINDOW_SIZE`. | +| `REALTIME_FETCH_CONCURRENCY` | `8` | Realtime RPC fetch concurrency for future live-mode fetchers. Must be at least `1`. | +| `RPC_TIMEOUT_MS` | `10000` | RPC request timeout budget in milliseconds for high-performance fetch clients. Must be non-negative. | +| `RPC_MAX_RETRIES` | `5` | Maximum RPC retry attempts for high-performance fetch clients. Must be non-negative. | +| `INGEST_CANDLES_INLINE` | `true` | Whether the process should run candle ingestion inline when that runtime path is enabled. | +| `INGEST_RESERVE_SNAPSHOTS_INLINE` | `true` | Whether the process should run reserve snapshot ingestion inline when that runtime path is enabled. | +| `INGEST_AGGREGATES_INLINE` | `false` | Whether the process should run aggregate ingestion inline when that runtime path is enabled. | +| `INGEST_BULK_STAGING_ENABLED` | `false` | Enables the catch-up-only staging-table merge writer when `INDEXER_MODE=catchup` and `INGEST_CANDLES_INLINE=false`. Leave unset/false for the default per-block ordered writer. | +| `READ_MODEL_REFRESH_INTERVAL_MS` | `15000` | Refresh cadence for API read-model tables used by `/stats`, `/pools`, candles, wallet history, and positions. Set `0` to disable periodic refreshes. | +| `API_PORT` | `8787` | Port for the HTTP API served by the same production process as the poller. | +| `PRICE_PROVIDER_BASE_URL` | unset | Reserved for a future provider worker. Current API only serves persisted `token_prices` rows. | +| `PRICE_PROVIDER_API_KEY` | unset | Reserved for future provider credentials; never commit real keys. | +| `PRICE_PROVIDER_NAME` | `provider` | Reserved source label for persisted provider writes. | +| `PRICE_CACHE_TTL_MS` | `300000` | Reserved for future provider/cache worker. | +| `PRICE_STALE_AFTER_MS` | `1800000` | Target age threshold for persisted price status. Current API returns stored `status`. | +| `PRICE_ALLOW_STALE` | `true` | Reserved for future resolver policy; current API never fabricates replacement prices. | +| `PRICE_DEV_MOCKS` | `false` | Reserved for local development only; production API in this package does not serve mocks. | + +## Local development + +```bash +cd services/indexer +npm ci +npm run typecheck +npm test +npm run build +``` + +Start Postgres and run migrations: + +```bash +cd services/indexer +docker compose up -d postgres +cp .env.example .env +npm run migrate +npm run dev +``` + +Run a bounded staging backfill from `START_HEIGHT` through a known smoke-test height, verify the cursor reached that height, then rebuild candles from ingested swaps: + +```bash +cd services/indexer +npm run backfill:range -- --to-height=39381355 +psql "$DATABASE_URL" -c "select id, last_height from indexer_cursors where id = 'astroport-juno-v1' and last_height >= 39381355;" +npm run backfill:candles -- --pair=juno1... --from=2026-07-01T00:00:00Z --to=2026-07-02T00:00:00Z --limit=10000 +``` + +The backfill reuses `token_candles`, keyed by `(chain_id, pair_address, asset, quote_asset, interval, bucket_start)`, and is idempotent with respect to inserted swap rows. Prices are derived from swap input/output amounts using a deterministic base/quote asset ordering; pass decimal metadata into the pure helpers when available for off-chain recalculation. + +A live RPC is only needed for `npm run dev`. Typecheck, tests, build, and SQL migration review do not need chain or database access. + +## Docker + +```bash +cd services/indexer +docker compose up --build +``` + +The `indexer` container waits on Postgres via Compose dependency, runs migrations, then starts the poller and API in one process. + +## Production deploy readiness + +This repository does not perform external deployment, DNS changes, or secret setup. The intended production shape is a containerized indexer/API service plus managed Postgres, exposed at one stable HTTPS origin that the frontend consumes through `VITE_DEX_INDEXER_URL`. + +Recommended platform settings: + +| Setting | Value | +| --- | --- | +| Build context | `services/indexer` | +| Dockerfile | `services/indexer/Dockerfile` | +| Start command | image default: `node dist/src/migrate.js && node dist/src/index.js` | +| Database | Managed Postgres with backups and point-in-time recovery enabled | +| Public URL | Stable HTTPS API origin, for example `https://indexer.` | +| Frontend config | Set `VITE_DEX_INDEXER_URL` in Vercel preview/production envs to this origin | + +Production environment variables should mirror `.env.example`, with these deployment-specific values set by the host secret manager: + +- `DATABASE_URL`: managed Postgres connection string; require TLS if the provider supports `?sslmode=require`. +- `START_HEIGHT`: factory deployment height for first backfill (`39381297` for the recorded Juno v1 deployment), not `1` unless a full-chain backfill is intentional. +- `JUNO_RPC_URL`, `JUNO_REST_URL`, `JUNO_WS_URL`: provider endpoints with agreed rate limits. +- `PRICE_PROVIDER_*`: reserved for future persisted price-worker integration; never commit real keys. + +Runbook for a release: + +1. Build and push the Docker image from `services/indexer` after `Indexer CI` passes. +2. Provision/attach managed Postgres and set the environment variables above. +3. Deploy one replica first; container startup runs migrations before the poller starts. +4. Confirm ingestion advances by checking logs for processed block ranges and by inspecting `indexer_cursors` in Postgres. +5. Expose the API/indexer service at the stable HTTPS URL, then set frontend `VITE_DEX_INDEXER_URL` for preview and production. +6. Smoke-check the frontend preview and production domains after the Vercel deploy completes. + +## Health checks and monitoring + +Use platform process health plus database/RPC smoke checks for the worker container: + +```bash +# Database connectivity from a one-off job/container with the same DATABASE_URL. +npm run migrate + +# Frontend-facing API read models. The main service refreshes these on startup +# and then every READ_MODEL_REFRESH_INTERVAL_MS; this command is useful for +# one-off repair or backfill validation. +npm run refresh:read-models + +# Cursor freshness: should move over time once the poller is running. +psql "$DATABASE_URL" -c "select id, last_height, updated_at from indexer_cursors order by updated_at desc limit 5;" +``` + +Expose `GET /health`, `GET /ready`, and `GET /metrics` at the same stable origin used by `VITE_DEX_INDEXER_URL`. The frontend client probes `/health` before reading `/stats`, `/pools`, `/prices`, `/wallets/:address/*`, and candle endpoints. Prometheus-compatible scrapers should use `/metrics` for readiness, cursor height, head height, confirmed target height, lag, confirmed lag, cursor age, RPC configured/reachable state, and migration count gauges. Treat `juno_indexer_rpc_reachable` as meaningful when `juno_indexer_rpc_configured` is `1`; local tests may omit RPC while production config should set `JUNO_RPC_URL`. The health response is JSON with at least: + +```json +{ "status": "ok", "chainId": "juno-1", "confirmationDepth": 2, "cursorHeight": 123456, "headHeight": 123500, "confirmedTargetHeight": 123498, "lag": 44, "confirmedLag": 42 } +``` + +Alert on: + +- container restart loops or non-zero exits; +- migration failures during deploy; +- Postgres CPU/storage/connection saturation; +- cursor lag above the agreed SLO, e.g. indexed height more than 50 confirmed blocks behind RPC head; +- repeated RPC rate-limit/network failures; +- API `/health` not returning `status: ok` or `/ready` not returning `status: ready`. + +## Ingestion model + +1. Read the `indexer_cursors` row for `astroport-juno-v1`. +2. Fetch the current chain head from `/status`. +3. Process blocks up to `head - CONFIRMATION_DEPTH` in bounded batches. +4. Fetch block metadata and block results via Tendermint RPC. +5. Normalize wasm events emitted by the factory, pairs, and incentives contracts. +6. Upsert pools, append immutable event rows, update position deltas, and advance the cursor in one core block transaction. +7. After the cursor advances, run best-effort height-pinned LCD reserve snapshot writes for known touched pairs. Snapshot failures are logged and can be repaired by later snapshot/backfill tooling; they do not roll back block/event persistence. +8. On restart, unique constraints make replay safe; block hashes in `processed_blocks` provide the basis for future rollback if a reorg is detected inside the confirmation window. + +## Notes for follow-up issues + +- Pool state snapshots are captured from height-pinned LCD smart queries when touched pairs emit swap/provide/withdraw events; USD/JUNO valuation logic still needs production-quality pricing and aggregate writes. API totals stay `null` until persisted aggregate data exists rather than returning synthetic zeroes. +- Candles are swap-derived from asset decimal metadata and store quote volume in `volume_quote`, not `volume_usd`. +- The frontend reads `VITE_DEX_INDEXER_URL`; point it at this service only after staging has backfilled real transaction data and `/ready` reports `ready`. diff --git a/indexer/dist/src/api-store.js b/indexer/dist/src/api-store.js new file mode 100644 index 000000000..2fb42bfb3 --- /dev/null +++ b/indexer/dist/src/api-store.js @@ -0,0 +1,398 @@ +import { JunoRpcClient } from "./rpc.js"; +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; +const MAX_CANDLE_LIMIT = 500; +const CANDLE_INTERVALS = new Set(["5m", "1h", "1d"]); +function limit(query, max = MAX_LIMIT) { + const parsed = Number.parseInt(query.limit ?? String(DEFAULT_LIMIT), 10); + if (!Number.isFinite(parsed) || parsed <= 0) + return DEFAULT_LIMIT; + return Math.min(parsed, max); +} +function offset(query) { + const parsed = Number.parseInt(query.cursor ?? "0", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; +} +function page(rows, query, max = MAX_LIMIT) { + const safeLimit = limit(query, max); + const start = offset(query); + return { data: rows, pagination: { limit: safeLimit, nextCursor: rows.length === safeLimit ? String(start + safeLimit) : null } }; +} +function toNumber(value) { + if (value === null || value === undefined) + return null; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} +function iso(value) { + if (!value) + return null; + return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString(); +} +function normalizeAssetInfo(value) { + if (typeof value === "string") + return value; + if (value && typeof value === "object") { + const obj = value; + if (typeof obj.native_token === "object" && obj.native_token) + return String(obj.native_token.denom ?? ""); + if (typeof obj.token === "object" && obj.token) + return String(obj.token.contract_addr ?? ""); + } + return String(value ?? ""); +} +function hasValue(value) { + return value !== null && value !== undefined; +} +function jsonArray(value) { + if (Array.isArray(value)) + return value; + if (typeof value !== "string") + return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } + catch { + return []; + } +} +function reserveAmountFor(asset, reserves) { + for (const reserve of reserves) { + if (!reserve || typeof reserve !== "object") + continue; + const row = reserve; + const denom = normalizeAssetInfo(row.denom ?? row.asset ?? row.info ?? row.asset_info); + if (denom === asset && hasValue(row.amount)) + return String(row.amount); + } + return null; +} +function baseAmount(value) { + const raw = String(value ?? "0"); + return /^\d+$/.test(raw) ? BigInt(raw) : 0n; +} +function decimalRatio(numerator, denominator) { + if (denominator <= 0n) + return 0; + const scaled = (numerator * 1000000000000n) / denominator; + return Number(scaled) / 1_000_000_000_000; +} +function prorateBaseAmount(amount, numerator, denominator) { + if (denominator <= 0n) + return "0"; + return ((baseAmount(amount) * numerator) / denominator).toString(); +} +function normalizePool(row) { + const assetInfos = Array.isArray(row.asset_infos) ? row.asset_infos : []; + const reserves = jsonArray(row.reserves); + const assets = assetInfos.map((asset) => { + const denom = normalizeAssetInfo(asset); + return { denom, reserve: reserveAmountFor(denom, reserves), valueUsd: null, valueJuno: null, priceUsd: null, priceJuno: null, priceStatus: "missing" }; + }); + const updatedAt = iso(row.updated_at ?? row.state_updated_at) ?? new Date(0).toISOString(); + return { + id: String(row.id ?? row.pool_id ?? row.pair_address), + pair: String(row.pair_address), + pairAddress: String(row.pair_address), + lpToken: row.liquidity_token_address ? String(row.liquidity_token_address) : null, + poolType: row.pool_type ? String(row.pool_type) : null, + assets, + totalShare: row.total_share ? String(row.total_share) : null, + tvlUsd: toNumber(row.tvl_usd), + tvlJuno: toNumber(row.tvl_juno), + volume24hUsd: toNumber(row.volume_24h_usd), + volume24hJuno: toNumber(row.volume_24h_juno), + volume7dUsd: toNumber(row.volume_7d_usd), + volume7dJuno: toNumber(row.volume_7d_juno), + fees24hUsd: toNumber(row.fees_24h_usd), + fees24hJuno: toNumber(row.fees_24h_juno), + feeBps: toNumber(row.fee_bps), + feeApr: toNumber(row.fee_apr) ?? 0, + incentivesApr: toNumber(row.incentives_apr) ?? 0, + totalApr: toNumber(row.total_apr) ?? 0, + incentivized: Boolean(row.incentivized), + updatedAt, + dataSource: "indexer", + isMock: false, + }; +} +function normalizePrice(row, asset) { + if (!row) + return { asset, priceUsd: null, priceJuno: null, source: null, status: "missing", stale: false, observedAt: null, ageMs: null, isMock: false }; + const observedAt = iso(row.observed_at); + const ageMs = observedAt ? Date.now() - new Date(observedAt).getTime() : null; + const status = String(row.status ?? (row.price_usd || row.price_juno ? "fresh" : "missing")); + return { asset: String(row.asset ?? asset), priceUsd: toNumber(row.price_usd), priceJuno: toNumber(row.price_juno), source: row.source ? String(row.source) : null, status, stale: status === "stale", observedAt, ageMs, isMock: false }; +} +export class PostgresApiStore { + db; + chainId; + cursorId; + rpc; + expectedMigrationCount; + expectedMigrationVersions; + confirmationDepth; + constructor(db, chainId, cursorId = "astroport-juno-v1", options = {}) { + this.db = db; + this.chainId = chainId; + this.cursorId = cursorId; + this.rpc = options.rpcUrl ? new JunoRpcClient(options.rpcUrl) : undefined; + this.expectedMigrationVersions = options.expectedMigrationVersions; + this.expectedMigrationCount = options.expectedMigrationCount ?? options.expectedMigrationVersions?.length; + this.confirmationDepth = Math.max(0, options.confirmationDepth ?? 0); + } + async chainHead() { + if (!this.rpc) + return null; + try { + return await this.rpc.head(); + } + catch { + return null; + } + } + healthFrom(cursorRow, head) { + const cursorHeight = toNumber(cursorRow?.last_height); + const cursorUpdatedAt = iso(cursorRow?.updated_at); + const cursorAgeMs = cursorUpdatedAt ? Math.max(0, Date.now() - new Date(cursorUpdatedAt).getTime()) : null; + const confirmedTargetHeight = head ? Math.max(0, head.height - this.confirmationDepth) : null; + return { + status: "ok", + service: "astroport-juno-indexer", + chainId: this.chainId, + confirmationDepth: this.confirmationDepth, + cursorHeight, + cursorBlockHash: cursorRow?.last_block_hash ? String(cursorRow.last_block_hash) : null, + cursorUpdatedAt, + cursorAgeMs, + headHeight: head?.height ?? null, + confirmedTargetHeight, + lag: head && cursorHeight !== null ? Math.max(0, head.height - cursorHeight) : null, + confirmedLag: confirmedTargetHeight !== null && cursorHeight !== null ? Math.max(0, confirmedTargetHeight - cursorHeight) : null, + rpcConfigured: Boolean(this.rpc), + rpcReachable: head !== null, + dataSource: "indexer", + isMock: false, + }; + } + readyFrom(appliedVersions, head) { + const migrationsApplied = appliedVersions.length; + const missingMigrations = this.expectedMigrationVersions?.filter((version) => !appliedVersions.includes(version)) ?? []; + const migrationsCurrent = this.expectedMigrationVersions + ? missingMigrations.length === 0 + : this.expectedMigrationCount === undefined || migrationsApplied >= this.expectedMigrationCount; + const rpcRequired = Boolean(this.rpc); + const rpcOk = !rpcRequired || head !== null; + return { + status: migrationsCurrent && rpcOk ? "ready" : "not_ready", + checks: { database: true, migrations: migrationsCurrent, rpc: rpcOk }, + database: "ok", + migrationsApplied, + expectedMigrations: this.expectedMigrationCount ?? null, + missingMigrations, + rpcConfigured: rpcRequired, + rpcReachable: head !== null, + headHeight: head?.height ?? null, + dataSource: "indexer", + isMock: false, + }; + } + async health() { + const [cursor, head] = await Promise.all([ + this.db.query(`SELECT last_height, last_block_hash, updated_at FROM indexer_cursors WHERE id = $1`, [this.cursorId]), + this.chainHead(), + ]); + return this.healthFrom(cursor.rows[0], head); + } + async ready() { + await this.db.query("SELECT 1"); + const [migrations, head] = await Promise.all([ + this.db.query(`SELECT version FROM schema_migrations ORDER BY version`), + this.chainHead(), + ]); + return this.readyFrom(migrations.rows.map((row) => row.version), head); + } + async opsStatus() { + const [cursor, migrations, head] = await Promise.all([ + this.db.query(`SELECT last_height, last_block_hash, updated_at FROM indexer_cursors WHERE id = $1`, [this.cursorId]), + (async () => { + await this.db.query("SELECT 1"); + return this.db.query(`SELECT version FROM schema_migrations ORDER BY version`); + })(), + this.chainHead(), + ]); + return { health: this.healthFrom(cursor.rows[0], head), ready: this.readyFrom(migrations.rows.map((row) => row.version), head) }; + } + async stats() { + const result = await this.db.query(`SELECT pool_count, incentivized_pools, updated_at, tvl_usd, tvl_juno, + volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, + fees_24h_usd, fees_24h_juno + FROM protocol_stats_latest + WHERE chain_id = $1`, [this.chainId]); + const row = result.rows[0] ?? {}; + return { + poolCount: Number(row.pool_count ?? 0), + tvlUsd: hasValue(row.tvl_usd) ? toNumber(row.tvl_usd) : null, + tvlJuno: hasValue(row.tvl_juno) ? toNumber(row.tvl_juno) : null, + volume24hUsd: hasValue(row.volume_24h_usd) ? toNumber(row.volume_24h_usd) : null, + volume24hJuno: hasValue(row.volume_24h_juno) ? toNumber(row.volume_24h_juno) : null, + volume7dUsd: hasValue(row.volume_7d_usd) ? toNumber(row.volume_7d_usd) : null, + volume7dJuno: hasValue(row.volume_7d_juno) ? toNumber(row.volume_7d_juno) : null, + fees24hUsd: hasValue(row.fees_24h_usd) ? toNumber(row.fees_24h_usd) : null, + fees24hJuno: hasValue(row.fees_24h_juno) ? toNumber(row.fees_24h_juno) : null, + incentivizedPools: Number(row.incentivized_pools ?? 0), + updatedAt: iso(row.updated_at) ?? new Date(0).toISOString(), + dataSource: "indexer", + isMock: false, + }; + } + async prices(assets) { + const result = await this.db.query(`SELECT DISTINCT ON (asset) asset, price_usd, price_juno, source, status, observed_at + FROM token_prices WHERE chain_id = $1 AND asset = ANY($2::text[]) + ORDER BY asset, observed_at DESC`, [this.chainId, assets]); + const byAsset = new Map(result.rows.map((row) => [String(row.asset), row])); + return assets.map((asset) => normalizePrice(byAsset.get(asset), asset)); + } + async pools(query) { + const safeLimit = limit(query); + const result = await this.db.query(`SELECT pool_id AS id, chain_id, pair_address, liquidity_token_address, pool_type, + asset_infos, created_height, created_tx_hash, first_seen_at, pool_updated_at AS updated_at, + reserves, total_share, tvl_usd, tvl_juno, volume_24h_usd, volume_24h_juno, + volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, state_updated_at + FROM latest_pool_state + WHERE chain_id = $1 AND ($2::text IS NULL OR pair_address = $2) + ORDER BY COALESCE(tvl_usd, 0) DESC, created_height DESC NULLS LAST + LIMIT $3 OFFSET $4`, [this.chainId, query.pair ?? null, safeLimit, offset(query)]); + return page(result.rows.map(normalizePool), query); + } + async pool(id) { + const readModel = await this.db.query(`SELECT pool_id AS id, chain_id, pair_address, liquidity_token_address, pool_type, + asset_infos, created_height, created_tx_hash, first_seen_at, pool_updated_at AS updated_at, + reserves, total_share, tvl_usd, tvl_juno, volume_24h_usd, volume_24h_juno, + volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, state_updated_at + FROM latest_pool_state + WHERE chain_id = $1 AND (pool_id::text = $2 OR pair_address = $2) LIMIT 1`, [this.chainId, id]); + if (readModel.rows[0]) + return normalizePool(readModel.rows[0]); + const result = await this.db.query(`SELECT p.*, lps.reserves, lps.total_share, lps.tvl_usd, lps.tvl_juno, lps.volume_24h_usd, lps.volume_24h_juno, + lps.volume_7d_usd, lps.volume_7d_juno, lps.fees_24h_usd, lps.fees_24h_juno, + lps.state_updated_at + FROM pools p + LEFT JOIN latest_pool_states lps ON lps.chain_id = p.chain_id AND lps.pair_address = p.pair_address + WHERE p.chain_id = $1 AND (p.id::text = $2 OR p.pair_address = $2) LIMIT 1`, [this.chainId, id]); + return result.rows[0] ? normalizePool(result.rows[0]) : null; + } + async candles(id, query) { + const interval = query.interval ?? "1h"; + if (!CANDLE_INTERVALS.has(interval)) + throw new RangeError(`unsupported interval: ${interval}`); + const pool = await this.pool(id); + if (!pool) + return null; + const pairAddress = String(pool.pairAddress); + const safeLimit = limit(query, MAX_CANDLE_LIMIT); + const values = [this.chainId, pairAddress, interval, query.baseAsset ?? null, query.quoteAsset ?? null, query.from ?? null, query.to ?? null, safeLimit, offset(query)]; + let result = await this.db.query(`SELECT pair_address, pool_id, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, trade_count + FROM pool_candle_buckets + WHERE chain_id = $1 AND pair_address = $2 AND interval = $3 + AND ($4::text IS NULL OR asset = $4) + AND ($5::text IS NULL OR quote_asset = $5) + AND ($6::timestamptz IS NULL OR bucket_start >= $6) + AND ($7::timestamptz IS NULL OR bucket_start <= $7) + ORDER BY bucket_start DESC LIMIT $8 OFFSET $9`, values); + const filterFallback = result.rows.length === 0 && Boolean(query.baseAsset || query.quoteAsset); + if (filterFallback) { + result = await this.db.query(`SELECT pair_address, pool_id, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, trade_count + FROM pool_candle_buckets + WHERE chain_id = $1 AND pair_address = $2 AND interval = $3 + AND ($4::timestamptz IS NULL OR bucket_start >= $4) + AND ($5::timestamptz IS NULL OR bucket_start <= $5) + ORDER BY bucket_start DESC LIMIT $6 OFFSET $7`, [this.chainId, pairAddress, interval, query.from ?? null, query.to ?? null, safeLimit, offset(query)]); + } + const data = result.rows.map((row) => ({ poolId: row.pool_id ? String(row.pool_id) : String(pool.id), pairAddress: String(row.pair_address), baseAsset: String(row.asset), quoteAsset: String(row.quote_asset), interval: String(row.interval), bucketStart: iso(row.bucket_start), open: toNumber(row.open), high: toNumber(row.high), low: toNumber(row.low), close: toNumber(row.close), volume: toNumber(row.volume), volumeQuote: toNumber(row.volume_quote), tradeCount: Number(row.trade_count ?? 0), dataSource: "indexer", isMock: false })); + return { ...page(data, query, MAX_CANDLE_LIMIT), meta: { poolId: String(pool.id), pairAddress, interval, baseAsset: filterFallback ? null : query.baseAsset ?? null, quoteAsset: filterFallback ? null : query.quoteAsset ?? null, requestedBaseAsset: query.baseAsset ?? null, requestedQuoteAsset: query.quoteAsset ?? null, filterFallback, from: query.from ?? null, to: query.to ?? null, dataSource: "indexer", isMock: false } }; + } + async poolPositions(id, query) { + const result = await this.db.query(`SELECT w.wallet_address AS owner_address, w.pool_id, w.pair_address, w.lp_token_address, + w.lp_balance, w.bonded_balance, w.updated_at, + lps.asset_infos, lps.reserves, lps.total_share, lps.tvl_usd, lps.tvl_juno + FROM wallet_position_latest w + LEFT JOIN latest_pool_state lps ON lps.chain_id = w.chain_id AND lps.pair_address = w.pair_address + WHERE w.chain_id = $1 AND (w.pool_id::text = $2 OR w.pair_address = $2) + ORDER BY w.updated_at DESC LIMIT $3 OFFSET $4`, [this.chainId, id, limit(query), offset(query)]); + return page(result.rows.map(normalizePosition), query); + } + async poolHistory(id, query) { + const pool = await this.pool(id); + if (!pool) + return page([], query); + const pairAddress = String(pool.pairAddress); + const result = await this.db.query(`SELECT tx_hash, wallet_address, pair_address, type, height, timestamp, + offer_asset, ask_asset, amount_usd, fee_usd, success + FROM wallet_history_flat + WHERE chain_id = $1 AND pair_address = $2 + ORDER BY height DESC, timestamp DESC LIMIT $3 OFFSET $4`, [this.chainId, pairAddress, limit(query), offset(query)]); + return page(result.rows.map(normalizeTx), query); + } + async walletPositions(addr, query) { + const result = await this.db.query(`SELECT w.wallet_address AS owner_address, w.pool_id, w.pair_address, w.lp_token_address, + w.lp_balance, w.bonded_balance, w.updated_at, + lps.asset_infos, lps.reserves, lps.total_share, lps.tvl_usd, lps.tvl_juno + FROM wallet_position_latest w + LEFT JOIN latest_pool_state lps ON lps.chain_id = w.chain_id AND lps.pair_address = w.pair_address + WHERE w.chain_id = $1 AND w.wallet_address = $2 + ORDER BY w.updated_at DESC LIMIT $3 OFFSET $4`, [this.chainId, addr, limit(query), offset(query)]); + return page(result.rows.map(normalizePosition), query); + } + async walletHistory(addr, query) { + const result = await this.db.query(`SELECT tx_hash, wallet_address, pair_address, type, height, timestamp, + offer_asset, ask_asset, amount_usd, fee_usd, success + FROM wallet_history_flat + WHERE chain_id = $1 AND wallet_address = $2 + ORDER BY height DESC, timestamp DESC LIMIT $3 OFFSET $4`, [this.chainId, addr, limit(query), offset(query)]); + return page(result.rows.map(normalizeTx), query); + } +} +function normalizePosition(row) { + const lpBalance = String(row.lp_balance ?? "0"); + const bondedBalance = String(row.bonded_balance ?? "0"); + const totalPositionLp = baseAmount(lpBalance) + baseAmount(bondedBalance); + const totalShare = baseAmount(row.total_share); + const share = decimalRatio(totalPositionLp, totalShare); + const assetInfos = Array.isArray(row.asset_infos) ? row.asset_infos : []; + const reserves = jsonArray(row.reserves); + const assets = assetInfos.map((asset) => { + const denom = normalizeAssetInfo(asset); + return { + denom, + reserve: reserveAmountFor(denom, reserves), + amount: prorateBaseAmount(reserveAmountFor(denom, reserves), totalPositionLp, totalShare), + valueUsd: null, + valueJuno: null, + priceUsd: null, + priceJuno: null, + priceStatus: "missing", + }; + }); + const tvlUsd = toNumber(row.tvl_usd); + const tvlJuno = toNumber(row.tvl_juno); + return { + walletAddress: String(row.owner_address), + poolId: String(row.pool_id ?? row.pair_address), + pairAddress: String(row.pair_address), + lpToken: row.lp_token_address ? String(row.lp_token_address) : null, + lpBalance, + bondedBalance, + shareBps: Math.round(share * 10_000), + valueUsd: tvlUsd === null || share <= 0 ? null : tvlUsd * share, + valueJuno: tvlJuno === null || share <= 0 ? null : tvlJuno * share, + assets, + updatedAt: iso(row.updated_at) ?? new Date(0).toISOString(), + dataSource: "indexer", + isMock: false, + }; +} +function normalizeTx(row) { + return { txHash: String(row.tx_hash), walletAddress: row.wallet_address ? String(row.wallet_address) : null, poolId: row.pair_address ? String(row.pair_address) : null, pairAddress: row.pair_address ? String(row.pair_address) : null, type: String(row.type), height: Number(row.height), timestamp: iso(row.timestamp) ?? new Date(0).toISOString(), offerAsset: row.offer_asset ?? null, askAsset: row.ask_asset ?? null, amountUsd: toNumber(row.amount_usd), feeUsd: toNumber(row.fee_usd), success: Boolean(row.success), dataSource: "indexer", isMock: false }; +} diff --git a/indexer/dist/src/api.js b/indexer/dist/src/api.js new file mode 100644 index 000000000..5f0615437 --- /dev/null +++ b/indexer/dist/src/api.js @@ -0,0 +1,169 @@ +import http from "node:http"; +import { URL } from "node:url"; +import { openApiDocument } from "./openapi.js"; +function baseHeaders(extraHeaders = {}) { + return { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, OPTIONS", + "access-control-allow-headers": "content-type, authorization", + ...extraHeaders, + }; +} +function jsonResponse(res, status, body, extraHeaders = {}) { + const payload = status === 204 ? "" : JSON.stringify(body); + res.writeHead(status, baseHeaders({ + "content-type": "application/json; charset=utf-8", + "cache-control": status === 200 ? "public, max-age=15, stale-while-revalidate=30" : "no-store", + ...extraHeaders, + })); + res.end(payload); +} +function textResponse(res, status, body, extraHeaders = {}) { + res.writeHead(status, baseHeaders({ + "content-type": "text/plain; version=0.0.4; charset=utf-8", + "cache-control": "no-store", + ...extraHeaders, + })); + res.end(body); +} +function query(searchParams) { + return { + limit: searchParams.get("limit") ?? undefined, + cursor: searchParams.get("cursor") ?? undefined, + pair: searchParams.get("pair") ?? undefined, + interval: searchParams.get("interval") ?? undefined, + from: searchParams.get("from") ?? undefined, + to: searchParams.get("to") ?? undefined, + baseAsset: searchParams.get("baseAsset") ?? searchParams.get("base_asset") ?? undefined, + quoteAsset: searchParams.get("quoteAsset") ?? searchParams.get("quote_asset") ?? undefined, + }; +} +function assets(searchParams, pathAsset) { + const values = []; + if (pathAsset) + values.push(pathAsset); + for (const key of ["asset", "assets", "denom", "denoms"]) { + const value = searchParams.get(key); + if (value) + values.push(...value.split(",")); + } + return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean))); +} +function metricHelp(name, help, type = "gauge") { + return [`# HELP ${name} ${help}`, `# TYPE ${name} ${type}`]; +} +function metricValue(value) { + if (typeof value === "boolean") + return value ? 1 : 0; + if (value === null || value === undefined) + return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} +function labelValue(value) { + return String(value ?? "unknown").replace(/[\\"\n]/g, "_"); +} +function metricLine(name, value, labels = {}) { + const number = metricValue(value); + if (number === null) + return null; + const labelEntries = Object.entries(labels); + const renderedLabels = labelEntries.length > 0 ? `{${labelEntries.map(([key, label]) => `${key}="${labelValue(label)}"`).join(",")}}` : ""; + return `${name}${renderedLabels} ${number}`; +} +async function metricsBody(store, metrics) { + const { health, ready } = await store.opsStatus(); + const labels = { chain_id: health.chainId ?? "unknown" }; + const lines = [ + ...metricHelp("juno_indexer_ready", "Indexer readiness status: 1 when /ready is ready, otherwise 0."), + metricLine("juno_indexer_ready", ready.status === "ready", labels), + ...metricHelp("juno_indexer_rpc_configured", "Whether this API store has an RPC endpoint configured for chain head checks."), + metricLine("juno_indexer_rpc_configured", health.rpcConfigured, labels), + ...metricHelp("juno_indexer_rpc_reachable", "RPC reachability; meaningful when juno_indexer_rpc_configured is 1."), + metricLine("juno_indexer_rpc_reachable", health.rpcReachable, labels), + ...metricHelp("juno_indexer_cursor_height", "Last block height committed to the indexer cursor."), + metricLine("juno_indexer_cursor_height", health.cursorHeight, labels), + ...metricHelp("juno_indexer_head_height", "Latest chain head height observed by the indexer API."), + metricLine("juno_indexer_head_height", health.headHeight, labels), + ...metricHelp("juno_indexer_confirmed_target_height", "Latest chain height considered safe after confirmation depth."), + metricLine("juno_indexer_confirmed_target_height", health.confirmedTargetHeight, labels), + ...metricHelp("juno_indexer_lag_blocks", "Difference between observed chain head and indexer cursor height."), + metricLine("juno_indexer_lag_blocks", health.lag, labels), + ...metricHelp("juno_indexer_confirmed_lag_blocks", "Difference between confirmed target height and indexer cursor height."), + metricLine("juno_indexer_confirmed_lag_blocks", health.confirmedLag, labels), + ...metricHelp("juno_indexer_cursor_age_ms", "Milliseconds since the indexer cursor row was last updated."), + metricLine("juno_indexer_cursor_age_ms", health.cursorAgeMs, labels), + ...metricHelp("juno_indexer_migrations_applied", "Number of schema migrations recorded as applied."), + metricLine("juno_indexer_migrations_applied", ready.migrationsApplied, labels), + ...metricHelp("juno_indexer_expected_migrations", "Expected schema migration count when configured."), + metricLine("juno_indexer_expected_migrations", ready.expectedMigrations, labels), + ]; + if (metrics) { + const snapshot = metrics.snapshot(); + lines.push(...metricHelp("juno_indexer_fetch_blocks_total", "Blocks fetched by the in-process indexer fetcher.", "counter"), metricLine("juno_indexer_fetch_blocks_total", snapshot.fetchBlocksTotal), ...metricHelp("juno_indexer_fetch_blocks_per_second", "Average block fetch throughput since process start."), metricLine("juno_indexer_fetch_blocks_per_second", snapshot.fetchBlocksPerSecond), ...metricHelp("juno_indexer_fetch_rpc_requests_in_flight", "RPC requests currently in flight."), metricLine("juno_indexer_fetch_rpc_requests_in_flight", snapshot.rpcRequestsInFlight), ...metricHelp("juno_indexer_fetch_rpc_error_total", "RPC fetch errors by low-cardinality status.", "counter")); + for (const [status, count] of snapshot.rpcErrors) + lines.push(metricLine("juno_indexer_fetch_rpc_error_total", count, { status })); + lines.push(...metricHelp("juno_indexer_decode_blocks_total", "Blocks decoded by the in-process indexer.", "counter"), metricLine("juno_indexer_decode_blocks_total", snapshot.decodeBlocksTotal), ...metricHelp("juno_indexer_writer_blocks_total", "Blocks committed by the indexer writer.", "counter"), metricLine("juno_indexer_writer_blocks_total", snapshot.writerBlocksTotal), ...metricHelp("juno_indexer_writer_commit_seconds", "Most recent block writer commit duration in seconds."), metricLine("juno_indexer_writer_commit_seconds", snapshot.writerCommitSeconds), ...metricHelp("juno_indexer_writer_events_total", "Events committed by the indexer writer by normalized kind.", "counter")); + for (const [kind, count] of snapshot.writerEvents) + lines.push(metricLine("juno_indexer_writer_events_total", count, { kind })); + lines.push(...metricHelp("juno_indexer_reorg_halt", "Whether ingestion is halted because of reorg protection."), metricLine("juno_indexer_reorg_halt", snapshot.reorgHalt)); + } + return `${lines.filter((line) => line !== null).join("\n")}\n`; +} +export function createIndexerApi(store, metrics) { + return http.createServer(async (req, res) => { + if (req.method === "OPTIONS") + return jsonResponse(res, 204, {}); + if (req.method !== "GET") + return jsonResponse(res, 405, { error: "method_not_allowed" }); + const url = new URL(req.url ?? "/", "http://localhost"); + const parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent); + const parsedQuery = query(url.searchParams); + try { + if (url.pathname === "/health") + return jsonResponse(res, 200, await store.health(), { "cache-control": "no-store" }); + if (url.pathname === "/ready") { + const body = await store.ready(); + return jsonResponse(res, body.status === "ready" ? 200 : 503, body, { "cache-control": "no-store" }); + } + if (url.pathname === "/metrics") + return textResponse(res, 200, await metricsBody(store, metrics)); + if (url.pathname === "/openapi.json") + return jsonResponse(res, 200, openApiDocument); + if (url.pathname === "/stats") + return jsonResponse(res, 200, await store.stats()); + if (parts[0] === "prices" && parts.length <= 2) { + const ids = assets(url.searchParams, parts[1]); + if (ids.length === 0) + return jsonResponse(res, 400, { error: "asset_required" }); + const prices = await store.prices(ids); + return jsonResponse(res, 200, parts[1] ? prices[0] ?? null : { data: prices }); + } + if (parts[0] === "pools" && parts.length === 1) + return jsonResponse(res, 200, await store.pools(parsedQuery)); + if (parts[0] === "pools" && parts.length === 2) { + const pool = await store.pool(parts[1]); + return pool ? jsonResponse(res, 200, pool) : jsonResponse(res, 404, { error: "pool_not_found" }); + } + if (parts[0] === "pools" && parts.length === 3 && parts[2] === "candles") { + const page = await store.candles(parts[1], parsedQuery); + return page ? jsonResponse(res, 200, page) : jsonResponse(res, 404, { error: "pool_not_found" }); + } + if (parts[0] === "pools" && parts.length === 3 && parts[2] === "positions") + return jsonResponse(res, 200, await store.poolPositions(parts[1], parsedQuery)); + if (parts[0] === "pools" && parts.length === 3 && parts[2] === "history") + return jsonResponse(res, 200, await store.poolHistory(parts[1], parsedQuery)); + if (parts[0] === "wallets" && parts.length === 3 && parts[2] === "positions") + return jsonResponse(res, 200, await store.walletPositions(parts[1], parsedQuery)); + if (parts[0] === "wallets" && parts.length === 3 && parts[2] === "history") + return jsonResponse(res, 200, await store.walletHistory(parts[1], parsedQuery)); + return jsonResponse(res, 404, { error: "not_found" }); + } + catch (error) { + if (error instanceof RangeError) + return jsonResponse(res, 400, { error: "bad_request", message: error.message }); + console.error("indexer_api_error", error); + return jsonResponse(res, 500, { error: "internal_error" }); + } + }); +} diff --git a/indexer/dist/src/backfill-candles.js b/indexer/dist/src/backfill-candles.js new file mode 100644 index 000000000..db7195548 --- /dev/null +++ b/indexer/dist/src/backfill-candles.js @@ -0,0 +1,64 @@ +import { loadConfig } from "./config.js"; +import { backfillTokenCandles, createPool } from "./db.js"; +const args = new Map(); +for (const arg of process.argv.slice(2)) { + const [key, value = ""] = arg.replace(/^--/, "").split("="); + if (key) + args.set(key, value); +} +const config = loadConfig(); +const pool = createPool(config); +const client = await pool.connect(); +try { + const chainId = args.get("chain-id") ?? config.chainId; + const pairAddress = args.get("pair") || undefined; + const from = args.get("from") || undefined; + const to = args.get("to") || undefined; + const processed = await backfillTokenCandles(client, { + chainId, + pairAddress, + from, + to, + batchSize: args.get("limit") ? Number(args.get("limit")) : undefined, + }); + console.log(`backfilled candle inputs processed=${processed}`); + const diagnostics = await client.query(`WITH selected_swaps AS ( + SELECT offer_asset, ask_asset + FROM swaps + WHERE chain_id = $1 + AND ($2::text IS NULL OR pair_address = $2) + AND ($3::timestamptz IS NULL OR block_time >= $3) + AND ($4::timestamptz IS NULL OR block_time <= $4) + ), + swap_assets AS ( + SELECT offer_asset AS asset FROM selected_swaps WHERE offer_asset IS NOT NULL + UNION + SELECT ask_asset AS asset FROM selected_swaps WHERE ask_asset IS NOT NULL + ), + asset_status AS ( + SELECT a.asset, m.decimals + FROM swap_assets a + LEFT JOIN asset_metadata m ON m.chain_id = $1 AND m.asset = a.asset + ), + eligible_swaps AS ( + SELECT 1 + FROM selected_swaps s + JOIN asset_metadata offer_meta ON offer_meta.chain_id = $1 AND offer_meta.asset = s.offer_asset AND offer_meta.decimals BETWEEN 0 AND 36 + JOIN asset_metadata ask_meta ON ask_meta.chain_id = $1 AND ask_meta.asset = s.ask_asset AND ask_meta.decimals BETWEEN 0 AND 36 + ) + SELECT + (SELECT count(*) FROM selected_swaps)::text AS swap_count, + (SELECT count(*) FROM eligible_swaps)::text AS eligible_swap_count, + (SELECT array_agg(asset ORDER BY asset) FROM asset_status WHERE decimals IS NULL OR decimals < 0 OR decimals > 36) AS missing_assets, + (SELECT count(*) FROM token_candles WHERE chain_id = $1 AND ($2::text IS NULL OR pair_address = $2) AND ($3::timestamptz IS NULL OR bucket_start >= $3) AND ($4::timestamptz IS NULL OR bucket_start <= $4))::text AS token_candle_count`, [chainId, pairAddress ?? null, from ?? null, to ?? null]); + const stats = diagnostics.rows[0]; + if (stats) { + console.log(`candle diagnostics swaps=${stats.swap_count} eligible_swaps=${stats.eligible_swap_count} token_candles=${stats.token_candle_count}`); + if (stats.missing_assets?.length) + console.log(`candle diagnostics missing_or_invalid_decimals=${stats.missing_assets.join(",")}`); + } +} +finally { + client.release(); + await pool.end(); +} diff --git a/indexer/dist/src/backfill-range.js b/indexer/dist/src/backfill-range.js new file mode 100644 index 000000000..1b1e5aa98 --- /dev/null +++ b/indexer/dist/src/backfill-range.js @@ -0,0 +1,35 @@ +import { parseNonNegativeInteger } from "./ranges.js"; +import { loadConfig } from "./config.js"; +import { createPool, runMigrations } from "./db.js"; +import { Indexer } from "./indexer.js"; +function intArg(name) { + const prefix = `--${name}=`; + const arg = process.argv.find((value) => value.startsWith(prefix)); + const raw = arg ? arg.slice(prefix.length) : process.env[name.toUpperCase().replace(/-/g, "_")]; + if (!raw) + return undefined; + return parseNonNegativeInteger(raw, name); +} +const toHeight = intArg("to-height"); +if (toHeight === undefined) + throw new Error("missing --to-height= or TO_HEIGHT"); +const config = loadConfig(); +const pool = createPool(config); +const indexer = new Indexer(config, pool); +let totalProcessed = 0; +try { + const applied = await runMigrations(pool); + if (applied.length > 0) + console.log(`migrations applied=${applied.join(",")}`); + for (;;) { + const result = await indexer.runUntilHeight(toHeight); + totalProcessed += result.processed; + console.log(`bounded backfill processed=${result.processed} total=${totalProcessed} head=${result.head} target=${result.target} cursor=${result.cursorHeight} to_height=${toHeight}`); + if (result.done) + break; + } +} +finally { + await indexer.close(); + await pool.end(); +} diff --git a/indexer/dist/src/benchmark-range.js b/indexer/dist/src/benchmark-range.js new file mode 100644 index 000000000..b0adeafc1 --- /dev/null +++ b/indexer/dist/src/benchmark-range.js @@ -0,0 +1,136 @@ +import { loadConfig } from "./config.js"; +import { createPool, runMigrations } from "./db.js"; +import { Indexer } from "./indexer.js"; +import { parseNonNegativeInteger } from "./ranges.js"; +function intArg(name) { + const prefix = `--${name}=`; + const arg = process.argv.find((value) => value.startsWith(prefix)); + const raw = arg ? arg.slice(prefix.length) : process.env[name.toUpperCase().replace(/-/g, "_")]; + if (!raw) + return undefined; + return parseNonNegativeInteger(raw, name); +} +function isRpcLikeError(error) { + const message = error instanceof Error ? error.message : String(error); + return /\b(RPC|LCD|fetch|status|block_results|\/block\?|\/status)\b/i.test(message); +} +async function setCursor(pool, params) { + await pool.query(`INSERT INTO indexer_cursors(id, chain_id, last_height, last_block_hash) + VALUES ($1, $2, $3, NULL) + ON CONFLICT (id) DO UPDATE + SET chain_id = EXCLUDED.chain_id, + last_height = EXCLUDED.last_height, + last_block_hash = NULL, + updated_at = now()`, [params.cursorId, params.chainId, params.height]); +} +async function getCursorHeight(pool, cursorId) { + const result = await pool.query(`SELECT last_height FROM indexer_cursors WHERE id = $1`, [cursorId]); + const raw = result.rows[0]?.last_height; + return raw === undefined ? null : Number(raw); +} +async function eventCounts(pool, chainId, fromHeight, toHeight) { + const [pools, swaps, liquidity, incentives] = await Promise.all([ + pool.query(`SELECT count(*)::text AS count + FROM pools + WHERE chain_id = $1 AND created_height BETWEEN $2 AND $3`, [chainId, fromHeight, toHeight]), + pool.query(`SELECT count(*)::text AS count + FROM swaps + WHERE chain_id = $1 AND height BETWEEN $2 AND $3`, [chainId, fromHeight, toHeight]), + pool.query(`SELECT kind, count(*)::text AS count + FROM liquidity_events + WHERE chain_id = $1 AND height BETWEEN $2 AND $3 + GROUP BY kind`, [chainId, fromHeight, toHeight]), + pool.query(`SELECT count(*)::text AS count + FROM incentive_events + WHERE chain_id = $1 AND height BETWEEN $2 AND $3`, [chainId, fromHeight, toHeight]), + ]); + const liquidityByKind = new Map(liquidity.rows.map((row) => [row.kind, Number(row.count)])); + return { + poolsCreated: Number(pools.rows[0]?.count ?? 0), + swaps: Number(swaps.rows[0]?.count ?? 0), + liquidityProvides: liquidityByKind.get("provide") ?? 0, + liquidityWithdraws: liquidityByKind.get("withdraw") ?? 0, + incentives: Number(incentives.rows[0]?.count ?? 0), + }; +} +const fromHeight = intArg("from-height"); +const toHeight = intArg("to-height"); +if (fromHeight === undefined) + throw new Error("missing --from-height= or FROM_HEIGHT"); +if (toHeight === undefined) + throw new Error("missing --to-height= or TO_HEIGHT"); +if (toHeight < fromHeight) + throw new Error("--to-height must be greater than or equal to --from-height"); +const config = loadConfig(); +const pool = createPool(config); +const indexer = new Indexer({ ...config, dryRun: false, startHeight: fromHeight }, pool); +const start = Date.now(); +let blocksProcessed = 0; +let cursor = null; +let head = null; +let target = null; +let rpcErrorCount = 0; +let migrationsApplied = []; +try { + migrationsApplied = await runMigrations(pool); + await setCursor(pool, { cursorId: config.cursorId, chainId: config.chainId, height: Math.max(0, fromHeight - 1) }); + for (;;) { + try { + const result = await indexer.runUntilHeight(toHeight); + blocksProcessed += result.processed; + cursor = result.cursorHeight; + head = result.head; + target = result.target; + if (result.done) + break; + } + catch (error) { + if (isRpcLikeError(error)) + rpcErrorCount += 1; + throw error; + } + } + cursor = await getCursorHeight(pool, config.cursorId); + const durationMs = Date.now() - start; + const counts = await eventCounts(pool, config.chainId, fromHeight, toHeight); + const summary = { + blockRange: { from: fromHeight, to: toHeight }, + durationMs, + durationSeconds: durationMs / 1000, + blocksProcessed, + blocksPerSecond: durationMs > 0 ? blocksProcessed / (durationMs / 1000) : blocksProcessed, + cursor, + head, + target, + lag: target === null || cursor === null ? null : Math.max(0, target - cursor), + rpcErrorCount, + eventCounts: counts, + migrationsApplied, + }; + console.log(JSON.stringify(summary)); +} +catch (error) { + const durationMs = Date.now() - start; + cursor = await getCursorHeight(pool, config.cursorId).catch(() => cursor); + const summary = { + blockRange: { from: fromHeight, to: toHeight }, + durationMs, + durationSeconds: durationMs / 1000, + blocksProcessed, + blocksPerSecond: durationMs > 0 ? blocksProcessed / (durationMs / 1000) : blocksProcessed, + cursor, + head, + target, + lag: target === null || cursor === null ? null : Math.max(0, target - cursor), + rpcErrorCount, + eventCounts: { poolsCreated: null, swaps: null, liquidityProvides: null, liquidityWithdraws: null, incentives: null }, + migrationsApplied, + error: error instanceof Error ? error.message : String(error), + }; + console.log(JSON.stringify(summary)); + process.exitCode = 1; +} +finally { + await indexer.close(); + await pool.end(); +} diff --git a/indexer/dist/src/block-fetcher.js b/indexer/dist/src/block-fetcher.js new file mode 100644 index 000000000..cabbc9472 --- /dev/null +++ b/indexer/dist/src/block-fetcher.js @@ -0,0 +1,39 @@ +export async function fetchBlockRange({ rpc, from, to, concurrency }) { + if (!Number.isInteger(from) || !Number.isInteger(to)) + throw new Error("block range bounds must be integer heights"); + if (!Number.isInteger(concurrency) || concurrency < 1) + throw new Error("block range concurrency must be an integer greater than or equal to 1"); + if (to < from) + return []; + const heights = Array.from({ length: to - from + 1 }, (_, index) => from + index); + const bundles = new Map(); + let nextIndex = 0; + let failed = false; + async function worker() { + for (;;) { + if (failed) + return; + const index = nextIndex; + nextIndex += 1; + const height = heights[index]; + if (height === undefined) + return; + try { + bundles.set(height, await rpc.block(height)); + } + catch (error) { + failed = true; + const message = error instanceof Error ? error.message : String(error); + throw new Error(`failed to fetch block ${height}: ${message}`, { cause: error }); + } + } + } + const workerCount = Math.min(concurrency, heights.length); + await Promise.all(Array.from({ length: workerCount }, () => worker())); + return heights.map((height) => { + const bundle = bundles.get(height); + if (!bundle) + throw new Error(`missing fetched block ${height}`); + return bundle; + }); +} diff --git a/indexer/dist/src/candle-worker.js b/indexer/dist/src/candle-worker.js new file mode 100644 index 000000000..8828540c6 --- /dev/null +++ b/indexer/dist/src/candle-worker.js @@ -0,0 +1,46 @@ +import { hostname } from "node:os"; +import { loadConfig } from "./config.js"; +import { createPool, processNextCandleJob } from "./db.js"; +const args = new Set(process.argv.slice(2)); +const config = loadConfig(); +const pool = createPool(config); +const workerId = process.env.CANDLE_WORKER_ID ?? `${hostname()}:${process.pid}`; +const pollMs = Number(process.env.CANDLE_WORKER_POLL_MS ?? config.pollIntervalMs); +const batchSize = Number(process.env.CANDLE_WORKER_BATCH_SIZE ?? 2_147_483_647); +const staleAfterMs = Number(process.env.CANDLE_WORKER_STALE_AFTER_MS ?? 10 * 60 * 1000); +async function withClient(fn) { + const client = await pool.connect(); + try { + return await fn(client); + } + finally { + client.release(); + } +} +async function runOnce() { + const job = await withClient((client) => processNextCandleJob(client, { + chainId: config.chainId, + workerId, + batchSize, + staleAfterMs, + })); + if (!job) + return false; + console.log(`candle worker processed job=${job.id} pair=${job.pairAddress} from=${job.fromTime} to=${job.toTime}`); + return true; +} +try { + if (args.has("--once")) { + await runOnce(); + } + else { + for (;;) { + const processed = await runOnce(); + if (!processed) + await new Promise((resolve) => setTimeout(resolve, pollMs)); + } + } +} +finally { + await pool.end(); +} diff --git a/indexer/dist/src/candles.js b/indexer/dist/src/candles.js new file mode 100644 index 000000000..922344eaa --- /dev/null +++ b/indexer/dist/src/candles.js @@ -0,0 +1,87 @@ +export const SUPPORTED_CANDLE_INTERVALS = ["5m", "1h", "1d"]; +const INTERVAL_MS = { + "5m": 5 * 60 * 1000, + "1h": 60 * 60 * 1000, + "1d": 24 * 60 * 60 * 1000, +}; +export function isCandleInterval(value) { + return SUPPORTED_CANDLE_INTERVALS.includes(value); +} +export function bucketStartFor(blockTime, interval) { + const date = blockTime instanceof Date ? blockTime : new Date(blockTime); + if (Number.isNaN(date.getTime())) + throw new Error(`invalid candle timestamp: ${blockTime}`); + return new Date(Math.floor(date.getTime() / INTERVAL_MS[interval]) * INTERVAL_MS[interval]).toISOString(); +} +function parsePositive(value) { + if (!value) + return undefined; + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; +} +function formatDecimal(value) { + if (!Number.isFinite(value)) + throw new Error("invalid candle decimal"); + return value.toPrecision(18).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1"); +} +export function deriveCanonicalSwapPrice(swap, decimals = {}) { + if (!swap.offerAsset || !swap.askAsset || swap.offerAsset === swap.askAsset) + return undefined; + const offerRaw = parsePositive(swap.offerAmount); + const returnRaw = parsePositive(swap.returnAmount); + if (!offerRaw || !returnRaw) + return undefined; + const offer = offerRaw / 10 ** (decimals[swap.offerAsset] ?? 0); + const returned = returnRaw / 10 ** (decimals[swap.askAsset] ?? 0); + if (offer <= 0 || returned <= 0) + return undefined; + const offerIsBase = swap.offerAsset < swap.askAsset; + const baseAsset = offerIsBase ? swap.offerAsset : swap.askAsset; + const quoteAsset = offerIsBase ? swap.askAsset : swap.offerAsset; + const baseVolume = offerIsBase ? offer : returned; + const quoteVolume = offerIsBase ? returned : offer; + const price = quoteVolume / baseVolume; + if (!Number.isFinite(price) || price <= 0) + return undefined; + return { + baseAsset, + quoteAsset, + price: formatDecimal(price), + volume: formatDecimal(baseVolume), + volumeQuote: formatDecimal(quoteVolume), + }; +} +export function aggregateSwapsToCandles(swaps, interval, decimals = {}) { + const buckets = new Map(); + for (const swap of swaps) { + const derived = deriveCanonicalSwapPrice(swap, decimals); + if (!derived) + continue; + const key = `${swap.pairAddress}:${derived.baseAsset}:${derived.quoteAsset}:${bucketStartFor(swap.blockTime, interval)}`; + const existing = buckets.get(key); + if (!existing) { + buckets.set(key, { + bucketStart: bucketStartFor(swap.blockTime, interval), + open: derived.price, + high: derived.price, + low: derived.price, + close: derived.price, + volume: Number(derived.volume), + volumeQuote: Number(derived.volumeQuote), + tradeCount: 1, + baseAsset: derived.baseAsset, + quoteAsset: derived.quoteAsset, + pairAddress: swap.pairAddress, + }); + } + else { + existing.high = formatDecimal(Math.max(Number(existing.high), Number(derived.price))); + existing.low = formatDecimal(Math.min(Number(existing.low), Number(derived.price))); + existing.close = derived.price; + existing.volume += Number(derived.volume); + existing.volumeQuote += Number(derived.volumeQuote); + existing.tradeCount += 1; + } + } + return [...buckets.values()].map((candle) => ({ ...candle, volume: formatDecimal(candle.volume), volumeQuote: formatDecimal(candle.volumeQuote) })); +} diff --git a/indexer/dist/src/config.js b/indexer/dist/src/config.js new file mode 100644 index 000000000..537dc8c33 --- /dev/null +++ b/indexer/dist/src/config.js @@ -0,0 +1,104 @@ +export const DEFAULT_CONTRACTS = { + factory: "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca", + router: "juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s", + incentives: "juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598", + oracle: "juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p", + nativeCoinRegistry: "juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2", +}; +export const DEFAULT_START_HEIGHT = 39_381_297; +function env(name, fallback) { + return process.env[name] ?? fallback; +} +process.loadEnvFile?.(".env"); +function intEnv(name, fallback, options = {}) { + const value = process.env[name]; + if (!value) + return fallback; + const trimmed = value.trim(); + if (!/^\d+$/.test(trimmed)) + throw new Error(`${name} must be ${options.label ?? "a non-negative integer"}`); + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isSafeInteger(parsed) || parsed < (options.min ?? 0)) { + throw new Error(`${name} must be ${options.label ?? "a non-negative integer"}`); + } + return parsed; +} +function boolEnv(name, fallback = false) { + const value = process.env[name]; + if (!value) + return fallback; + return ["1", "true", "yes", "y"].includes(value.toLowerCase()); +} +function indexerModeEnv() { + const value = env("INDEXER_MODE", "realtime"); + if (value === "realtime" || value === "catchup") + return value; + throw new Error('INDEXER_MODE must be either "realtime" or "catchup"'); +} +function deriveWsUrl(rpcUrl) { + const url = new URL(rpcUrl); + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; + url.pathname = "/websocket"; + return url.toString(); +} +export function loadConfig() { + const rpcUrl = env("JUNO_RPC_URL", "https://juno-rpc.publicnode.com:443").replace(/\/$/, ""); + const fetchWindowSize = intEnv("FETCH_WINDOW_SIZE", 250, { + min: 1, + label: "an integer greater than or equal to 1", + }); + const fetchConcurrency = intEnv("FETCH_CONCURRENCY", 32, { + min: 1, + label: "an integer greater than or equal to 1", + }); + if (fetchConcurrency > fetchWindowSize) { + throw new Error("FETCH_CONCURRENCY must be less than or equal to FETCH_WINDOW_SIZE"); + } + return { + databaseUrl: env("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/astroport_indexer"), + rpcUrl, + restUrl: env("JUNO_REST_URL", "https://juno-rest.publicnode.com").replace(/\/$/, ""), + wsUrl: env("JUNO_WS_URL", deriveWsUrl(rpcUrl)), + chainId: env("CHAIN_ID", "juno-1"), + factoryAddress: env("FACTORY_ADDRESS", DEFAULT_CONTRACTS.factory), + routerAddress: env("ROUTER_ADDRESS", DEFAULT_CONTRACTS.router), + incentivesAddress: env("INCENTIVES_ADDRESS", DEFAULT_CONTRACTS.incentives), + oracleAddress: env("ORACLE_ADDRESS", DEFAULT_CONTRACTS.oracle), + nativeCoinRegistryAddress: env("NATIVE_COIN_REGISTRY_ADDRESS", DEFAULT_CONTRACTS.nativeCoinRegistry), + startHeight: intEnv("START_HEIGHT", DEFAULT_START_HEIGHT), + confirmationDepth: intEnv("CONFIRMATION_DEPTH", 2), + pollIntervalMs: intEnv("POLL_INTERVAL_MS", 5_000), + batchSize: intEnv("BATCH_SIZE", 20, { + min: 1, + label: "an integer greater than or equal to 1", + }), + dryRun: boolEnv("DRY_RUN"), + cursorId: env("CURSOR_ID", "astroport-juno-v1"), + indexerMode: indexerModeEnv(), + rangeSize: intEnv("RANGE_SIZE", 5_000, { + min: 1, + label: "an integer greater than or equal to 1", + }), + fetchWindowSize, + fetchConcurrency, + realtimeFetchConcurrency: intEnv("REALTIME_FETCH_CONCURRENCY", 8, { + min: 1, + label: "an integer greater than or equal to 1", + }), + rpcTimeoutMs: intEnv("RPC_TIMEOUT_MS", 10_000), + rpcMaxRetries: intEnv("RPC_MAX_RETRIES", 5), + ingestCandlesInline: boolEnv("INGEST_CANDLES_INLINE", true), + ingestReserveSnapshotsInline: boolEnv("INGEST_RESERVE_SNAPSHOTS_INLINE", true), + ingestAggregatesInline: boolEnv("INGEST_AGGREGATES_INLINE", false), + ingestBulkStagingEnabled: boolEnv("INGEST_BULK_STAGING_ENABLED", false), + priceProviderBaseUrl: process.env.PRICE_PROVIDER_BASE_URL || undefined, + priceProviderApiKey: process.env.PRICE_PROVIDER_API_KEY || undefined, + priceProviderName: env("PRICE_PROVIDER_NAME", "provider"), + priceCacheTtlMs: intEnv("PRICE_CACHE_TTL_MS", 300_000), + priceStaleAfterMs: intEnv("PRICE_STALE_AFTER_MS", 1_800_000), + priceAllowStale: boolEnv("PRICE_ALLOW_STALE", true), + priceDevMocks: boolEnv("PRICE_DEV_MOCKS"), + readModelRefreshIntervalMs: intEnv("READ_MODEL_REFRESH_INTERVAL_MS", 15_000), + apiPort: intEnv("API_PORT", 8787), + }; +} diff --git a/indexer/dist/src/db.js b/indexer/dist/src/db.js new file mode 100644 index 000000000..e4c88ef4a --- /dev/null +++ b/indexer/dist/src/db.js @@ -0,0 +1,676 @@ +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import pg from "pg"; +import { aggregateSwapsToCandles, bucketStartFor, deriveCanonicalSwapPrice, SUPPORTED_CANDLE_INTERVALS } from "./candles.js"; +const { Pool } = pg; +export function createPool(config) { + return new Pool({ connectionString: config.databaseUrl, max: 5 }); +} +const DEFAULT_MIGRATIONS_DIR = join(process.cwd(), "migrations"); +export async function listMigrationFiles(migrationsDir = DEFAULT_MIGRATIONS_DIR) { + return (await readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort(); +} +export async function runMigrations(pool, migrationsDir = DEFAULT_MIGRATIONS_DIR) { + const files = await listMigrationFiles(migrationsDir); + await pool.query(`CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())`); + const existing = await pool.query("SELECT version FROM schema_migrations"); + const alreadyApplied = new Set(existing.rows.map((row) => row.version)); + const applied = []; + for (const file of files) { + if (alreadyApplied.has(file)) + continue; + const sql = await readFile(join(migrationsDir, file), "utf8"); + await pool.query("BEGIN"); + try { + await pool.query(sql); + await pool.query("INSERT INTO schema_migrations(version) VALUES($1) ON CONFLICT DO NOTHING", [file]); + await pool.query("COMMIT"); + applied.push(file); + } + catch (error) { + await pool.query("ROLLBACK"); + throw error; + } + } + return applied; +} +export async function getCursor(client, cursorId, chainId, startHeight) { + const result = await client.query(`INSERT INTO indexer_cursors(id, chain_id, last_height) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO UPDATE SET updated_at = now() + RETURNING last_height`, [cursorId, chainId, Math.max(0, startHeight - 1)]); + return Number(result.rows[0]?.last_height ?? Math.max(0, startHeight - 1)); +} +export async function recordProcessedBlock(client, params) { + const existing = await client.query(`SELECT block_hash, parent_hash FROM processed_blocks WHERE chain_id = $1 AND height = $2`, [params.chainId, params.height]); + const existingBlock = existing.rows[0]; + if (existingBlock && existingBlock.block_hash !== params.blockHash) { + throw new Error(`processed block hash mismatch at height ${params.height}: existing=${existingBlock.block_hash} incoming=${params.blockHash}`); + } + if (params.parentHash) { + const previous = await client.query(`SELECT block_hash FROM processed_blocks WHERE chain_id = $1 AND height = $2 - 1`, [params.chainId, params.height]); + const previousHash = previous.rows[0]?.block_hash; + if (previousHash && previousHash !== params.parentHash) { + throw new Error(`processed block parent hash mismatch at height ${params.height}: previous=${previousHash} incoming_parent=${params.parentHash}`); + } + } + const written = await client.query(`INSERT INTO processed_blocks(chain_id, height, block_hash, parent_hash, block_time, tx_count) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (height) DO UPDATE + SET chain_id = EXCLUDED.chain_id, + block_time = EXCLUDED.block_time, + tx_count = EXCLUDED.tx_count, + processed_at = now(), + parent_hash = COALESCE(processed_blocks.parent_hash, EXCLUDED.parent_hash) + WHERE processed_blocks.chain_id = EXCLUDED.chain_id + AND processed_blocks.block_hash = EXCLUDED.block_hash + AND ( + processed_blocks.parent_hash IS NULL + OR EXCLUDED.parent_hash IS NULL + OR processed_blocks.parent_hash = EXCLUDED.parent_hash + )`, [params.chainId, params.height, params.blockHash, params.parentHash ?? null, params.blockTime, params.txCount]); + if (written.rowCount === 0) { + throw new Error(`processed block conflict at height ${params.height}: existing row differs from incoming block`); + } +} +export async function advanceCursor(client, params) { + await client.query(`UPDATE indexer_cursors SET last_height = $2, last_block_hash = $3, updated_at = now() WHERE id = $1`, [params.cursorId, params.height, params.blockHash]); +} +export async function upsertPoolStateSnapshot(client, params) { + const pool = await client.query(`SELECT id FROM pools WHERE chain_id = $1 AND pair_address = $2`, [params.chainId, params.pairAddress]); + const poolId = pool.rows[0]?.id; + if (!poolId) + throw new Error(`cannot write pool state snapshot for unknown pair ${params.pairAddress}`); + await client.query(`INSERT INTO pool_state_snapshots(pool_id, height, block_time, reserves, total_share, source) + VALUES ($1, $2, $3, $4::jsonb, $5, $6) + ON CONFLICT (pool_id, height, source) DO UPDATE + SET block_time = EXCLUDED.block_time, + reserves = EXCLUDED.reserves, + total_share = EXCLUDED.total_share`, [poolId, params.height, params.blockTime, JSON.stringify(params.reserves), params.totalShare ?? null, params.source ?? "event"]); +} +export async function enqueueSnapshotJobs(client, params) { + const pairAddresses = [...new Set(params.pairAddresses.filter(Boolean))]; + if (pairAddresses.length === 0) + return 0; + const result = await client.query(`INSERT INTO snapshot_jobs(chain_id, pair_address, height, block_time, reason) + SELECT $1, p.pair_address, $3, $4, $5 + FROM pools p + WHERE p.chain_id = $1 + AND p.pair_address = ANY($2::text[]) + ON CONFLICT (chain_id, pair_address, height, reason) DO NOTHING`, [params.chainId, pairAddresses, params.height, params.blockTime, params.reason]); + return result.rowCount ?? 0; +} +export async function claimSnapshotJobs(client, params) { + const result = await client.query(`WITH claimable AS ( + SELECT id + FROM snapshot_jobs + WHERE chain_id = $1 + AND status IN ('pending', 'leased') + AND attempts < $4 + AND (status = 'pending' OR leased_until <= now() OR leased_until IS NULL) + ORDER BY id ASC + LIMIT $2 + FOR UPDATE SKIP LOCKED + ) + UPDATE snapshot_jobs j + SET status = 'leased', + attempts = j.attempts + 1, + leased_until = now() + ($3::text)::interval, + updated_at = now() + FROM claimable + WHERE j.id = claimable.id + RETURNING j.id, j.chain_id, j.pair_address, j.height, j.block_time, j.reason, j.status, j.attempts`, [params.chainId, params.limit, `${params.leaseSeconds} seconds`, params.maxAttempts]); + return result.rows.map((row) => ({ + id: String(row.id), + chainId: row.chain_id, + pairAddress: row.pair_address, + height: Number(row.height), + blockTime: row.block_time, + reason: row.reason, + status: row.status, + attempts: Number(row.attempts), + })); +} +export async function markSnapshotJobSucceeded(client, params) { + await client.query(`UPDATE snapshot_jobs + SET status = 'succeeded', leased_until = NULL, last_error = NULL, updated_at = now() + WHERE id = $1 + AND status = 'leased' + AND attempts = $2`, [params.jobId, params.attempt]); +} +export async function markSnapshotJobFailed(client, params) { + await client.query(`UPDATE snapshot_jobs + SET status = CASE WHEN $3::boolean OR attempts >= $5 THEN 'failed' ELSE 'pending' END, + leased_until = NULL, + last_error = $4, + updated_at = now() + WHERE id = $1 + AND status = 'leased' + AND attempts = $2`, [params.jobId, params.attempt, params.permanent, params.error.slice(0, 2_000), params.maxAttempts]); +} +async function multiInsert(client, table, columns, rows, conflict = "DO NOTHING") { + if (rows.length === 0) + return; + const values = []; + const tuples = rows.map((row) => { + const placeholders = columns.map((column) => { + values.push(column.value(row)); + return `$${values.length}${column.cast ? `::${column.cast}` : ""}`; + }); + return `(${placeholders.join(",")})`; + }); + await client.query(`INSERT INTO ${table}(${columns.map((column) => column.name).join(",")}) VALUES ${tuples.join(",")} ON CONFLICT ${conflict}`, values); +} +export async function stageAndMergeBatch(client, params) { + if (params.blocks.length === 0) + return; + const batchId = params.batchId; + await stageProcessedBlocks(client, batchId, params.blocks); + await stageEvents(client, batchId, params.chainId, params.blocks.flatMap((block) => block.events)); + await mergeStagedBatch(client, params); +} +async function stageProcessedBlocks(client, batchId, blocks) { + await multiInsert(client, "stage_processed_blocks", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: (block) => block.chainId }, + { name: "height", value: (block) => block.height }, + { name: "block_hash", value: (block) => block.blockHash }, + { name: "parent_hash", value: (block) => block.parentHash ?? null }, + { name: "block_time", value: (block) => block.blockTime }, + { name: "tx_count", value: (block) => block.txCount }, + ], blocks); +} +async function stageEvents(client, batchId, chainId, events) { + await multiInsert(client, "stage_pools", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "factory_address", value: (event) => event.factoryAddress }, + { name: "pair_address", value: (event) => event.pairAddress }, + { name: "liquidity_token_address", value: (event) => event.liquidityTokenAddress ?? null }, + { name: "pool_type", value: (event) => event.poolType ?? null }, + { name: "asset_infos", value: (event) => JSON.stringify(event.assetInfos), cast: "jsonb" }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event) => event.kind === "pool_created")); + await multiInsert(client, "stage_swaps", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "pair_address", value: (event) => event.pairAddress }, + { name: "trader", value: (event) => event.trader ?? null }, + { name: "offer_asset", value: (event) => event.offerAsset ?? null }, + { name: "offer_amount", value: (event) => event.offerAmount ?? null }, + { name: "ask_asset", value: (event) => event.askAsset ?? null }, + { name: "return_amount", value: (event) => event.returnAmount ?? null }, + { name: "spread_amount", value: (event) => event.spreadAmount ?? null }, + { name: "commission_amount", value: (event) => event.commissionAmount ?? null }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event) => event.kind === "swap")); + await multiInsert(client, "stage_liquidity_events", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "pair_address", value: (event) => event.pairAddress }, + { name: "kind", value: (event) => event.kind }, + { name: "provider", value: (event) => event.provider ?? null }, + { name: "assets", value: (event) => JSON.stringify(event.assets), cast: "jsonb" }, + { name: "share_amount", value: (event) => event.shareAmount ?? null }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event) => event.kind === "provide" || event.kind === "withdraw")); + await multiInsert(client, "stage_incentive_events", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "incentives_address", value: (event) => event.incentivesAddress }, + { name: "lp_token_address", value: (event) => event.lpTokenAddress ?? null }, + { name: "user_address", value: (event) => event.userAddress ?? null }, + { name: "action", value: (event) => event.action }, + { name: "amount", value: (event) => event.amount ?? null }, + { name: "reward_asset", value: (event) => event.rewardAsset ?? null }, + { name: "reward_amount", value: (event) => event.rewardAmount ?? null }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event) => event.kind === "incentive")); +} +async function mergeStagedBatch(client, params) { + const batchId = params.batchId; + await validateStagedBlockContinuity(client, params.chainId, params.blocks); + const blockResult = await client.query(`INSERT INTO processed_blocks(chain_id, height, block_hash, parent_hash, block_time, tx_count) + SELECT chain_id, height, block_hash, parent_hash, block_time, tx_count + FROM stage_processed_blocks + WHERE batch_id = $1::uuid AND chain_id = $2 + ORDER BY height ASC + ON CONFLICT (height) DO UPDATE + SET chain_id = EXCLUDED.chain_id, + block_time = EXCLUDED.block_time, + tx_count = EXCLUDED.tx_count, + processed_at = now(), + parent_hash = COALESCE(processed_blocks.parent_hash, EXCLUDED.parent_hash) + WHERE processed_blocks.chain_id = EXCLUDED.chain_id + AND processed_blocks.block_hash = EXCLUDED.block_hash + AND ( + processed_blocks.parent_hash IS NULL + OR EXCLUDED.parent_hash IS NULL + OR processed_blocks.parent_hash = EXCLUDED.parent_hash + )`, [batchId, params.chainId]); + if ((blockResult.rowCount ?? 0) !== params.blocks.length) + throw new Error(`processed block conflict while merging staging batch ${batchId}`); + await client.query(`INSERT INTO pools(chain_id, pair_address, factory_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, first_seen_at) + SELECT chain_id, pair_address, factory_address, liquidity_token_address, pool_type, asset_infos, height, tx_hash, block_time + FROM stage_pools + WHERE batch_id = $1::uuid AND chain_id = $2 + ORDER BY height ASC, msg_index ASC, event_index ASC + ON CONFLICT (chain_id, pair_address) DO UPDATE + SET liquidity_token_address = COALESCE(EXCLUDED.liquidity_token_address, pools.liquidity_token_address), + pool_type = COALESCE(EXCLUDED.pool_type, pools.pool_type), + asset_infos = CASE WHEN jsonb_array_length(EXCLUDED.asset_infos) > 0 THEN EXCLUDED.asset_infos ELSE pools.asset_infos END, + updated_at = now()`, [batchId, params.chainId]); + await client.query(`INSERT INTO swaps(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, trader, + offer_asset, offer_amount, ask_asset, return_amount, spread_amount, commission_amount, raw_event) + SELECT s.chain_id, p.id, s.pair_address, s.height, s.block_time, s.tx_hash, s.msg_index, s.event_index, s.trader, + s.offer_asset, s.offer_amount, s.ask_asset, s.return_amount, s.spread_amount, s.commission_amount, s.raw_event + FROM stage_swaps s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + WHERE s.batch_id = $1::uuid AND s.chain_id = $2 + ORDER BY s.height ASC, s.msg_index ASC, s.event_index ASC + ON CONFLICT DO NOTHING`, [batchId, params.chainId]); + await client.query(`INSERT INTO liquidity_events(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, kind, provider, assets, share_amount, raw_event) + SELECT s.chain_id, p.id, s.pair_address, s.height, s.block_time, s.tx_hash, s.msg_index, s.event_index, s.kind, s.provider, s.assets, s.share_amount, s.raw_event + FROM stage_liquidity_events s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + WHERE s.batch_id = $1::uuid AND s.chain_id = $2 + ORDER BY s.height ASC, s.msg_index ASC, s.event_index ASC + ON CONFLICT DO NOTHING`, [batchId, params.chainId]); + await client.query(`INSERT INTO incentive_events(chain_id, incentives_address, lp_token_address, user_address, action, amount, reward_asset, reward_amount, + height, block_time, tx_hash, msg_index, event_index, raw_event) + SELECT chain_id, incentives_address, lp_token_address, user_address, action, amount, reward_asset, reward_amount, + height, block_time, tx_hash, msg_index, event_index, raw_event + FROM stage_incentive_events + WHERE batch_id = $1::uuid AND chain_id = $2 + ORDER BY height ASC, msg_index ASC, event_index ASC + ON CONFLICT DO NOTHING`, [batchId, params.chainId]); + if (params.writeCandlesInline === false) { + await client.query(`INSERT INTO candle_jobs(chain_id, pair_address, from_time, to_time, status, run_after) + SELECT DISTINCT s.chain_id, s.pair_address, + date_trunc('day', s.block_time AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' AS from_time, + (date_trunc('day', s.block_time AT TIME ZONE 'UTC') AT TIME ZONE 'UTC') + interval '1 day' AS to_time, + 'pending', now() + FROM stage_swaps s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + WHERE s.batch_id = $1::uuid AND s.chain_id = $2 + ON CONFLICT (chain_id, pair_address, from_time, to_time) DO UPDATE + SET status = CASE WHEN candle_jobs.status = 'running' THEN candle_jobs.status ELSE 'pending' END, + rerun_requested = CASE WHEN candle_jobs.status = 'running' THEN true ELSE false END, + run_after = now(), + last_error = NULL, + updated_at = now()`, [batchId, params.chainId]); + } + if (params.enqueueSnapshots) { + await client.query(`INSERT INTO snapshot_jobs(chain_id, pair_address, height, block_time, reason) + SELECT DISTINCT s.chain_id, s.pair_address, s.height, s.block_time, 'touched' + FROM ( + SELECT chain_id, pair_address, height, block_time FROM stage_swaps WHERE batch_id = $1::uuid AND chain_id = $2 + UNION + SELECT chain_id, pair_address, height, block_time FROM stage_liquidity_events WHERE batch_id = $1::uuid AND chain_id = $2 + ) s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + ON CONFLICT (chain_id, pair_address, height, reason) DO NOTHING`, [batchId, params.chainId]); + } + const lastBlock = params.blocks[params.blocks.length - 1]; + await advanceCursor(client, { cursorId: params.cursorId, height: lastBlock.height, blockHash: lastBlock.blockHash }); + await client.query(`UPDATE stage_processed_blocks SET merged_at = now() WHERE batch_id = $1::uuid AND chain_id = $2`, [batchId, params.chainId]); + await cleanupSuccessfulStagingBatches(client, { chainId: params.chainId, olderThanHours: params.cleanupOlderThanHours ?? 24 }); +} +async function validateStagedBlockContinuity(client, chainId, blocks) { + const ordered = [...blocks].sort((a, b) => a.height - b.height); + for (let index = 1; index < ordered.length; index += 1) { + const previous = ordered[index - 1]; + const current = ordered[index]; + if (current.height !== previous.height + 1) + throw new Error(`non-contiguous staging batch at height ${current.height}`); + if (current.parentHash && current.parentHash !== previous.blockHash) { + throw new Error(`parent hash mismatch for staged block ${current.height}`); + } + } + const first = ordered[0]; + if (!first?.parentHash) + return; + const result = await client.query(`SELECT block_hash FROM processed_blocks WHERE chain_id = $1 AND height = $2`, [chainId, first.height - 1]); + const previous = result.rows[0]; + if (previous && previous.block_hash !== first.parentHash) { + throw new Error(`parent hash mismatch for staged block ${first.height}`); + } +} +export async function cleanupSuccessfulStagingBatches(client, params) { + const olderThan = `${params.olderThanHours ?? 24} hours`; + await client.query(`WITH old_batches AS ( + SELECT DISTINCT batch_id FROM stage_processed_blocks + WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval + ) + DELETE FROM stage_pools WHERE batch_id IN (SELECT batch_id FROM old_batches)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_swaps WHERE batch_id IN (SELECT batch_id FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_liquidity_events WHERE batch_id IN (SELECT batch_id FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_incentive_events WHERE batch_id IN (SELECT batch_id FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval`, [params.chainId, olderThan]); +} +export async function writeNormalizedEvents(client, chainId, events, options = {}) { + for (const event of events) { + if (event.kind === "pool_created") + await upsertPool(client, chainId, event); + } + for (const event of events) { + if (event.kind !== "pool_created") + await writeNormalizedEvent(client, chainId, event, options); + } +} +export async function writeNormalizedEvent(client, chainId, event, options = {}) { + switch (event.kind) { + case "pool_created": + return upsertPool(client, chainId, event); + case "swap": + return insertSwap(client, chainId, event, options); + case "provide": + case "withdraw": + return insertLiquidityEvent(client, chainId, event); + case "incentive": + return insertIncentiveEvent(client, chainId, event); + } +} +async function upsertPool(client, chainId, event) { + await client.query(`INSERT INTO pools(chain_id, pair_address, factory_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, first_seen_at) + VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9) + ON CONFLICT (chain_id, pair_address) DO UPDATE + SET liquidity_token_address = COALESCE(EXCLUDED.liquidity_token_address, pools.liquidity_token_address), + pool_type = COALESCE(EXCLUDED.pool_type, pools.pool_type), + asset_infos = CASE WHEN jsonb_array_length(EXCLUDED.asset_infos) > 0 THEN EXCLUDED.asset_infos ELSE pools.asset_infos END, + updated_at = now()`, [chainId, event.pairAddress, event.factoryAddress, event.liquidityTokenAddress ?? null, event.poolType ?? null, JSON.stringify(event.assetInfos), event.height, event.txHash, event.blockTime]); +} +async function poolIdForPair(client, chainId, pairAddress) { + const pool = await client.query(`SELECT id FROM pools WHERE chain_id = $1 AND pair_address = $2`, [chainId, pairAddress]); + return pool.rows[0]?.id ?? null; +} +async function insertSwap(client, chainId, event, options) { + const poolId = await poolIdForPair(client, chainId, event.pairAddress); + if (!poolId) + return; + const inserted = await client.query(`INSERT INTO swaps(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, trader, + offer_asset, offer_amount, ask_asset, return_amount, spread_amount, commission_amount, raw_event) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16::jsonb) + ON CONFLICT DO NOTHING + RETURNING id, pool_id`, [ + chainId, + poolId, + event.pairAddress, + event.height, + event.blockTime, + event.txHash, + event.msgIndex, + event.eventIndex, + event.trader ?? null, + event.offerAsset ?? null, + event.offerAmount ?? null, + event.askAsset ?? null, + event.returnAmount ?? null, + event.spreadAmount ?? null, + event.commissionAmount ?? null, + JSON.stringify(event.raw), + ]); + if (inserted.rowCount === 0) + return; + if (options.writeCandlesInline === false) { + await enqueueCandleJobForSwap(client, chainId, event.pairAddress, event.blockTime); + } + else { + await upsertCandlesForSwap(client, chainId, event, poolId); + } +} +function addMilliseconds(iso, ms) { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) + throw new Error(`invalid candle job timestamp: ${iso}`); + return new Date(date.getTime() + ms).toISOString(); +} +export async function enqueueCandleJobForSwap(client, chainId, pairAddress, blockTime) { + const fromTime = bucketStartFor(blockTime, "1d"); + const toTime = addMilliseconds(fromTime, 24 * 60 * 60 * 1000); + await client.query(`INSERT INTO candle_jobs(chain_id, pair_address, from_time, to_time, status, run_after) + VALUES ($1, $2, $3, $4, 'pending', now()) + ON CONFLICT (chain_id, pair_address, from_time, to_time) DO UPDATE + SET status = CASE WHEN candle_jobs.status = 'running' THEN candle_jobs.status ELSE 'pending' END, + rerun_requested = CASE WHEN candle_jobs.status = 'running' THEN true ELSE false END, + run_after = now(), + last_error = NULL, + updated_at = now()`, [chainId, pairAddress, fromTime, toTime]); +} +const MAX_ASSET_DECIMALS = 36; +const assetDecimalsCache = new Map(); +function decimalsCacheKey(chainId, asset) { + return `${chainId}:${asset}`; +} +function isValidAssetDecimals(value) { + if (value === null || value === undefined || value === "") + return false; + const parsed = Number(value); + return Number.isInteger(parsed) && parsed >= 0 && parsed <= MAX_ASSET_DECIMALS; +} +function hasCompleteDecimals(decimals, assets) { + return assets.every((asset) => Boolean(asset) && decimals[asset] !== undefined); +} +async function loadAssetDecimals(client, chainId, assets) { + const uniqueAssets = [...new Set(assets.filter((asset) => Boolean(asset)))]; + if (uniqueAssets.length === 0) + return {}; + const decimals = {}; + const missing = []; + for (const asset of uniqueAssets) { + const cached = assetDecimalsCache.get(decimalsCacheKey(chainId, asset)); + if (cached === undefined) + missing.push(asset); + else + decimals[asset] = cached; + } + if (missing.length > 0) { + const result = await client.query(`SELECT asset, decimals FROM asset_metadata WHERE chain_id = $1 AND asset = ANY($2::text[])`, [chainId, missing]); + for (const row of result.rows) { + if (!row.asset || !isValidAssetDecimals(row.decimals)) + continue; + const parsed = Number(row.decimals); + assetDecimalsCache.set(decimalsCacheKey(chainId, row.asset), parsed); + decimals[row.asset] = parsed; + } + } + return decimals; +} +async function upsertCandlesForSwap(client, chainId, event, poolId) { + const decimals = await loadAssetDecimals(client, chainId, [event.offerAsset, event.askAsset]); + if (!hasCompleteDecimals(decimals, [event.offerAsset, event.askAsset])) + return; + const derived = deriveCanonicalSwapPrice({ + pairAddress: event.pairAddress, + blockTime: event.blockTime, + offerAsset: event.offerAsset, + offerAmount: event.offerAmount, + askAsset: event.askAsset, + returnAmount: event.returnAmount, + }, decimals); + if (!derived) + return; + for (const interval of SUPPORTED_CANDLE_INTERVALS) { + await client.query(`INSERT INTO token_candles(chain_id, pool_id, pair_address, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, volume_usd, trade_count, source) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$8,$8,$8,$9,$10,NULL,1,'indexer') + ON CONFLICT (chain_id, pair_address, asset, quote_asset, interval, bucket_start) DO UPDATE + SET high = GREATEST(token_candles.high, EXCLUDED.high), + low = LEAST(token_candles.low, EXCLUDED.low), + close = EXCLUDED.close, + volume = token_candles.volume + EXCLUDED.volume, + volume_quote = COALESCE(token_candles.volume_quote, 0) + COALESCE(EXCLUDED.volume_quote, 0), + volume_usd = NULL, + trade_count = token_candles.trade_count + 1, + pool_id = COALESCE(token_candles.pool_id, EXCLUDED.pool_id), + updated_at = now()`, [chainId, poolId, event.pairAddress, derived.baseAsset, derived.quoteAsset, interval, bucketStartFor(event.blockTime, interval), derived.price, derived.volume, derived.volumeQuote]); + } +} +export async function backfillTokenCandles(client, params = { chainId: "juno-1" }) { + return rebuildTokenCandlesForRange(client, { ...params, source: "backfill", toExclusive: false }); +} +export async function rebuildTokenCandlesForRange(client, params = { chainId: "juno-1" }) { + const result = await client.query(`SELECT pair_address, block_time, offer_asset, offer_amount, ask_asset, return_amount, + $1::text AS chain_id, height, tx_hash, msg_index, event_index + FROM swaps + WHERE chain_id = $1 + AND ($2::text IS NULL OR pair_address = $2) + AND ($3::timestamptz IS NULL OR block_time >= $3) + AND ($4::timestamptz IS NULL OR (($6::boolean AND block_time < $4) OR (NOT $6::boolean AND block_time <= $4))) + ORDER BY height ASC, msg_index ASC, event_index ASC, id ASC + LIMIT $5`, [params.chainId, params.pairAddress ?? null, params.from ?? null, params.to ?? null, params.batchSize ?? 10_000, params.toExclusive ?? false]); + const swaps = result.rows.map((row) => ({ + pairAddress: row.pair_address, + blockTime: row.block_time, + offerAsset: row.offer_asset, + offerAmount: row.offer_amount, + askAsset: row.ask_asset, + returnAmount: row.return_amount, + })); + const poolIds = new Map(); + for (const row of result.rows) { + if (poolIds.has(row.pair_address)) + continue; + const pool = await client.query(`SELECT id FROM pools WHERE chain_id = $1 AND pair_address = $2`, [params.chainId, row.pair_address]); + poolIds.set(row.pair_address, pool.rows[0]?.id ?? null); + } + const decimals = await loadAssetDecimals(client, params.chainId, swaps.flatMap((swap) => [swap.offerAsset, swap.askAsset])); + const swapsWithDecimals = swaps.filter((swap) => hasCompleteDecimals(decimals, [swap.offerAsset, swap.askAsset])); + for (const interval of SUPPORTED_CANDLE_INTERVALS) { + const candles = aggregateSwapsToCandles(swapsWithDecimals, interval, decimals); + for (const candle of candles) { + await client.query(`INSERT INTO token_candles(chain_id, pool_id, pair_address, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, volume_usd, trade_count, source) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,NULL,$14,$15) + ON CONFLICT (chain_id, pair_address, asset, quote_asset, interval, bucket_start) DO UPDATE + SET open = EXCLUDED.open, + high = EXCLUDED.high, + low = EXCLUDED.low, + close = EXCLUDED.close, + volume = EXCLUDED.volume, + volume_quote = EXCLUDED.volume_quote, + volume_usd = NULL, + trade_count = EXCLUDED.trade_count, + pool_id = COALESCE(token_candles.pool_id, EXCLUDED.pool_id), + source = EXCLUDED.source, + updated_at = now()`, [params.chainId, poolIds.get(candle.pairAddress) ?? null, candle.pairAddress, candle.baseAsset, candle.quoteAsset, interval, candle.bucketStart, candle.open, candle.high, candle.low, candle.close, candle.volume, candle.volumeQuote, candle.tradeCount, params.source ?? "backfill"]); + } + } + return result.rowCount ?? 0; +} +function mapCandleJob(row) { + return { + id: String(row.id), + chainId: row.chain_id, + pairAddress: row.pair_address, + fromTime: row.from_time, + toTime: row.to_time, + attempts: Number(row.attempts), + workerId: row.worker_id, + }; +} +export async function claimNextCandleJob(client, params) { + const result = await client.query(`WITH next_job AS ( + SELECT id + FROM candle_jobs + WHERE chain_id = $1 + AND run_after <= now() + AND ( + status IN ('pending', 'failed') + OR (status = 'running' AND claimed_at < now() - (($3::text)::interval)) + ) + ORDER BY run_after ASC, created_at ASC, id ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED + ) + UPDATE candle_jobs + SET status = 'running', + attempts = attempts + 1, + worker_id = $2, + claimed_at = now(), + last_error = NULL, + updated_at = now() + FROM next_job + WHERE candle_jobs.id = next_job.id + RETURNING candle_jobs.id, chain_id, pair_address, from_time, to_time, attempts, worker_id`, [params.chainId, params.workerId, `${params.staleAfterMs ?? 10 * 60 * 1000} milliseconds`]); + const row = result.rows[0]; + return row ? mapCandleJob(row) : undefined; +} +export async function completeCandleJob(client, job, processedSwaps) { + await client.query(`UPDATE candle_jobs + SET status = CASE WHEN rerun_requested THEN 'pending' ELSE 'completed' END, + rerun_requested = false, + processed_swaps = $4, + updated_at = now() + WHERE id = $1 + AND status = 'running' + AND worker_id = $2 + AND attempts = $3`, [job.id, job.workerId, job.attempts, processedSwaps]); +} +export async function failCandleJob(client, job, error) { + const message = error instanceof Error ? error.message : String(error); + await client.query(`UPDATE candle_jobs + SET status = 'failed', last_error = $4, run_after = now() + (($5::text)::interval), updated_at = now() + WHERE id = $1 + AND status = 'running' + AND worker_id = $2 + AND attempts = $3`, [job.id, job.workerId, job.attempts, message.slice(0, 2_000), "30 seconds"]); +} +export async function processNextCandleJob(client, params) { + const job = await claimNextCandleJob(client, params); + if (!job) + return undefined; + try { + const processed = await rebuildTokenCandlesForRange(client, { + chainId: job.chainId, + pairAddress: job.pairAddress, + from: job.fromTime, + to: job.toTime, + batchSize: params.batchSize ?? 2_147_483_647, + source: "worker", + toExclusive: true, + }); + await completeCandleJob(client, job, processed); + return job; + } + catch (error) { + await failCandleJob(client, job, error); + throw error; + } +} +export async function refreshApiReadModels(client, params = {}) { + const result = await client.query(`SELECT model, rows_affected FROM refresh_api_read_models($1::text)`, [params.chainId ?? null]); + return result.rows.map((row) => ({ model: row.model, rowsAffected: Number(row.rows_affected) })); +} +async function insertLiquidityEvent(client, chainId, event) { + const poolId = await poolIdForPair(client, chainId, event.pairAddress); + if (!poolId) + return; + await client.query(`INSERT INTO liquidity_events(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, kind, provider, assets, share_amount, raw_event) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13::jsonb) + ON CONFLICT DO NOTHING`, [chainId, poolId, event.pairAddress, event.height, event.blockTime, event.txHash, event.msgIndex, event.eventIndex, event.kind, event.provider ?? null, JSON.stringify(event.assets), event.shareAmount ?? null, JSON.stringify(event.raw)]); +} +async function insertIncentiveEvent(client, chainId, event) { + await client.query(`INSERT INTO incentive_events(chain_id, incentives_address, lp_token_address, user_address, action, amount, reward_asset, reward_amount, + height, block_time, tx_hash, msg_index, event_index, raw_event) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb) + ON CONFLICT DO NOTHING`, [chainId, event.incentivesAddress, event.lpTokenAddress ?? null, event.userAddress ?? null, event.action, event.amount ?? null, event.rewardAsset ?? null, event.rewardAmount ?? null, event.height, event.blockTime, event.txHash, event.msgIndex, event.eventIndex, JSON.stringify(event.raw)]); +} diff --git a/indexer/dist/src/events.js b/indexer/dist/src/events.js new file mode 100644 index 000000000..f8ce6966a --- /dev/null +++ b/indexer/dist/src/events.js @@ -0,0 +1,140 @@ +export function attributesToRecord(attributes) { + const out = {}; + for (const { key, value } of attributes) { + const current = out[key]; + if (current === undefined) + out[key] = value; + else if (Array.isArray(current)) + current.push(value); + else + out[key] = [current, value]; + } + return out; +} +function first(raw, keys) { + for (const key of keys) { + const value = raw[key]; + if (Array.isArray(value)) + return value[0]; + if (value) + return value; + } + return undefined; +} +function all(raw, keys) { + const values = []; + for (const key of keys) { + const value = raw[key]; + if (Array.isArray(value)) + values.push(...value); + else if (value) + values.push(value); + } + return values; +} +function parseAssets(raw) { + const denoms = all(raw, ["assets", "asset", "withdrawn_assets", "refund_assets", "provided_assets", "offer_asset", "ask_asset"]); + const amounts = all(raw, ["amounts", "amount", "withdrawn_amounts", "provided_amounts"]); + if (denoms.length === 0 && amounts.length === 0) + return []; + if (denoms.length === 1 && amounts.length === 0) + return parseCoinList(denoms[0]); + if (denoms.length === amounts.length) + return denoms.map((asset, index) => ({ asset, amount: amounts[index] })); + return denoms.map((asset) => ({ asset })); +} +function parseCoinList(value) { + return value.split(",").map((part) => part.trim()).filter(Boolean).map((coin) => { + const match = coin.match(/^(\d+)(.+)$/); + return match ? { amount: match[1], asset: match[2] } : { asset: coin }; + }); +} +function isWasm(event) { + return event.type === "wasm" || event.type.startsWith("wasm-") || event.type === "execute"; +} +export function normalizeWasmEvent(event, context, contracts) { + if (!isWasm(event)) + return undefined; + const raw = attributesToRecord(event.attributes); + const action = first(raw, ["action", "method", "_contract_action"]); + const contract = first(raw, ["_contract_address", "contract_address"]); + if (!action || !contract) + return undefined; + if (contract === contracts.factoryAddress && ["create_pair", "pair_created", "create_pair_and_distribution_flows", "register"].includes(action)) { + const pairAddress = first(raw, ["pair_contract_addr", "pair_address", "contract_addr", "pair"]); + if (!pairAddress || !pairAddress.startsWith("juno1")) + return undefined; + return { + ...context, + kind: "pool_created", + factoryAddress: contract, + pairAddress, + liquidityTokenAddress: first(raw, ["liquidity_token_addr", "liquidity_token", "lp_token_addr"]), + poolType: first(raw, ["pair_type", "pool_type"]), + assetInfos: all(raw, ["asset_infos", "asset_info", "assets"]), + raw, + }; + } + if (["swap", "swap_and_send"].includes(action)) { + return { + ...context, + kind: "swap", + pairAddress: contract, + trader: first(raw, ["sender", "trader", "receiver"]), + offerAsset: first(raw, ["offer_asset", "offer_asset_info", "ask_asset"]), + offerAmount: first(raw, ["offer_amount", "amount"]), + askAsset: first(raw, ["ask_asset", "ask_asset_info", "return_asset"]), + returnAmount: first(raw, ["return_amount", "return"]), + spreadAmount: first(raw, ["spread_amount"]), + commissionAmount: first(raw, ["commission_amount"]), + raw, + }; + } + if (["provide_liquidity", "provide"].includes(action)) { + return { + ...context, + kind: "provide", + pairAddress: contract, + provider: first(raw, ["sender", "provider", "receiver"]), + assets: parseAssets(raw), + shareAmount: first(raw, ["share", "share_amount", "minted_share"]), + raw, + }; + } + if (["withdraw_liquidity", "withdraw"].includes(action)) { + return { + ...context, + kind: "withdraw", + pairAddress: contract, + provider: first(raw, ["sender", "provider", "receiver"]), + assets: parseAssets(raw), + shareAmount: first(raw, ["share", "share_amount", "refund_share", "withdrawn_share"]), + raw, + }; + } + if (contract === contracts.incentivesAddress) { + return { + ...context, + kind: "incentive", + incentivesAddress: contract, + action, + lpTokenAddress: first(raw, ["lp_token", "lp_token_addr", "staking_token", "staking_token_addr"]), + userAddress: first(raw, ["user", "sender", "staker", "recipient"]), + amount: first(raw, ["amount", "bond_amount", "unbond_amount"]), + rewardAsset: first(raw, ["reward_asset", "reward_token", "asset"]), + rewardAmount: first(raw, ["reward_amount", "rewards", "amount"]), + raw, + }; + } + return undefined; +} +export function normalizeBlockEvents(events, baseContext, contracts) { + return events + .map((event, eventIndex) => normalizeWasmEvent(event, { ...baseContext, msgIndex: inferMsgIndex(event), eventIndex }, contracts)) + .filter((event) => event !== undefined); +} +function inferMsgIndex(event) { + const raw = attributesToRecord(event.attributes); + const value = first(raw, ["msg_index", "msg_index_start"]); + return value ? Number.parseInt(value, 10) || 0 : 0; +} diff --git a/indexer/dist/src/index.js b/indexer/dist/src/index.js new file mode 100644 index 000000000..40125ad11 --- /dev/null +++ b/indexer/dist/src/index.js @@ -0,0 +1,32 @@ +import { createIndexerApi } from "./api.js"; +import { PostgresApiStore } from "./api-store.js"; +import { loadConfig } from "./config.js"; +import { createPool, listMigrationFiles, runMigrations } from "./db.js"; +import { Indexer } from "./indexer.js"; +import { indexerMetrics } from "./metrics.js"; +import { ReadModelRefresher } from "./read-model-refresher.js"; +const config = loadConfig(); +const pool = createPool(config); +const appliedMigrations = await runMigrations(pool); +console.log(`migrations checked: ${appliedMigrations.join(", ")}`); +const expectedMigrationVersions = await listMigrationFiles(); +const api = createIndexerApi(new PostgresApiStore(pool, config.chainId, config.cursorId, { rpcUrl: config.rpcUrl, expectedMigrationVersions, confirmationDepth: config.confirmationDepth }), indexerMetrics); +const indexer = new Indexer(config, pool, indexerMetrics); +const readModels = new ReadModelRefresher(pool, { chainId: config.chainId, intervalMs: config.readModelRefreshIntervalMs }); +await readModels.refreshOnce().catch((error) => { + console.warn("indexer_read_models_initial_refresh_failed", { error: error instanceof Error ? error.message : String(error) }); +}); +readModels.start(); +await new Promise((resolve) => api.listen(config.apiPort, resolve)); +console.log(`astroport juno indexer api listening on :${config.apiPort}`); +async function shutdown(signal) { + console.log(`received ${signal}; shutting down indexer`); + readModels.stop(); + await new Promise((resolve, reject) => api.close((error) => (error ? reject(error) : resolve()))); + await indexer.close(); + await pool.end(); + process.exit(0); +} +process.on("SIGINT", () => void shutdown("SIGINT")); +process.on("SIGTERM", () => void shutdown("SIGTERM")); +await indexer.runForever(); diff --git a/indexer/dist/src/indexer.js b/indexer/dist/src/indexer.js new file mode 100644 index 000000000..8e4801984 --- /dev/null +++ b/indexer/dist/src/indexer.js @@ -0,0 +1,257 @@ +import { randomUUID } from "node:crypto"; +import { fetchBlockRange } from "./block-fetcher.js"; +import { advanceCursor, createPool, enqueueSnapshotJobs, getCursor, recordProcessedBlock, stageAndMergeBatch, upsertPoolStateSnapshot, writeNormalizedEvents } from "./db.js"; +import { normalizeBlockEvents } from "./events.js"; +import { JunoRestClient, JunoRpcClient } from "./rpc.js"; +import { nextBlockRange } from "./ranges.js"; +export class Indexer { + config; + metrics; + rpc; + rest; + pool; + ownsPool; + constructor(config, pool, metrics) { + this.config = config; + this.metrics = metrics; + this.rpc = new JunoRpcClient(config.rpcUrl, { metrics, timeoutMs: config.rpcTimeoutMs, maxRetries: config.rpcMaxRetries }); + this.rest = new JunoRestClient(config.restUrl); + this.pool = pool ?? (config.dryRun ? undefined : createPool(config)); + this.ownsPool = !pool && !config.dryRun; + } + async close() { + if (this.ownsPool) + await this.pool?.end(); + } + async runOnce(maxHeight) { + const planned = await this.planRange(maxHeight); + if (planned.empty) + return { processed: 0, head: planned.head.height, target: planned.target, cursorHeight: planned.lastHeight }; + const rangeStartedAt = Date.now(); + const blocks = await this.fetchRange(planned.from, planned.to); + const decoded = blocks.map((block) => this.normalizeBlock(block)); + for (const _block of decoded) + this.metrics?.recordDecodedBlock(); + const eventCounts = countRangeEvents(decoded); + let dbDurationMs = 0; + const writeStartedAt = Date.now(); + let processed = 0; + try { + processed = this.shouldUseBulkStaging() + ? await this.writeBulkStagingBatch(decoded) + : await this.writeBlocksInOrder(decoded); + dbDurationMs = Date.now() - writeStartedAt; + } + catch (error) { + dbDurationMs = Date.now() - writeStartedAt; + if (isReorgHaltError(error)) + this.metrics?.setReorgHalt(true); + throw error; + } + if (!this.config.dryRun) { + this.metrics?.recordWriterEvents(eventCounts); + if (processed > 0) + this.metrics?.recordWriterBlock(dbDurationMs / 1000); + } + this.metrics?.setReorgHalt(false); + console.log(JSON.stringify({ + msg: "indexer_range_processed", + role: "indexer", + rangeFrom: planned.from, + rangeTo: planned.to, + cursor: planned.to, + head: planned.head.height, + target: planned.target, + lag: Math.max(0, planned.target - planned.to), + blocks: processed, + swaps: eventCounts.swap ?? 0, + liquidityEvents: (eventCounts.provide ?? 0) + (eventCounts.withdraw ?? 0), + incentiveEvents: eventCounts.incentive ?? 0, + durationMs: Date.now() - rangeStartedAt, + dbDurationMs, + })); + return { processed, head: planned.head.height, target: planned.target, cursorHeight: planned.to }; + } + async runForever() { + for (;;) { + const result = await this.runOnce(); + console.log(`indexer loop processed=${result.processed} head=${result.head} target=${result.target}`); + await new Promise((resolve) => setTimeout(resolve, this.config.pollIntervalMs)); + } + } + async runUntilHeight(maxHeight) { + const result = await this.runOnce(maxHeight); + if (result.cursorHeight < maxHeight && result.processed === 0) { + throw new Error(`confirmed target ${result.target} is below requested to-height ${maxHeight}; cursor is at ${result.cursorHeight}`); + } + return { ...result, done: result.cursorHeight >= maxHeight }; + } + async planRange(maxHeight) { + const head = await this.rpc.head(); + const target = Math.max(0, head.height - this.config.confirmationDepth); + const lastHeight = this.config.dryRun + ? Math.max(0, this.config.startHeight - 1) + : await withClient(this.pool, (client) => getCursor(client, this.config.cursorId, this.config.chainId, this.config.startHeight)); + const { from, to, empty } = nextBlockRange({ lastHeight, confirmedTarget: target, batchSize: this.config.batchSize, maxHeight }); + return { head, target, lastHeight, from, to, empty }; + } + fetchRange(from, to) { + return fetchBlockRange({ + rpc: this.rpc, + from, + to, + concurrency: this.config.indexerMode === "catchup" ? this.config.fetchConcurrency : this.config.realtimeFetchConcurrency, + }); + } + normalizeBlock(block) { + const events = block.txEvents.flatMap((tx) => normalizeBlockEvents(tx.events, { chainId: this.config.chainId, height: block.height, blockTime: block.time, txHash: tx.txHash }, { factoryAddress: this.config.factoryAddress, incentivesAddress: this.config.incentivesAddress })); + return { block, events }; + } + shouldUseBulkStaging() { + return !this.config.dryRun + && this.config.indexerMode === "catchup" + && this.config.ingestBulkStagingEnabled + && !this.config.ingestCandlesInline; + } + async writeBulkStagingBatch(blocks) { + if (blocks.length === 0) + return 0; + await withClient(this.pool, async (client) => { + await client.query("BEGIN"); + try { + await stageAndMergeBatch(client, { + batchId: randomUUID(), + chainId: this.config.chainId, + cursorId: this.config.cursorId, + blocks: blocks.map(({ block, events }) => ({ + chainId: this.config.chainId, + height: block.height, + blockHash: block.hash, + parentHash: block.parentHash, + blockTime: block.time, + txCount: block.txCount, + events, + })), + writeCandlesInline: this.config.ingestCandlesInline, + enqueueSnapshots: !this.config.ingestReserveSnapshotsInline, + }); + await client.query("COMMIT"); + } + catch (error) { + await client.query("ROLLBACK"); + throw error; + } + }); + if (this.config.ingestReserveSnapshotsInline) { + for (const { block, events } of blocks) { + await this.writeReserveSnapshots(events, block.height, block.time); + } + } + return blocks.length; + } + async writeBlocksInOrder(blocks) { + let processed = 0; + for (const { block, events } of blocks) { + if (this.config.dryRun) { + console.log(JSON.stringify({ height: block.height, hash: block.hash, events }, null, 2)); + } + else { + await this.writeBlock(block, events); + if (this.config.ingestReserveSnapshotsInline) { + await this.writeReserveSnapshots(events, block.height, block.time); + } + } + processed += 1; + } + return processed; + } + async writeBlock(block, events) { + await withClient(this.pool, async (client) => { + await client.query("BEGIN"); + try { + await recordProcessedBlock(client, { + chainId: this.config.chainId, + height: block.height, + blockHash: block.hash, + parentHash: block.parentHash, + blockTime: block.time, + txCount: block.txCount, + }); + await writeNormalizedEvents(client, this.config.chainId, events, { writeCandlesInline: this.config.ingestCandlesInline }); + if (!this.config.ingestReserveSnapshotsInline) { + await enqueueSnapshotJobs(client, { + chainId: this.config.chainId, + pairAddresses: touchedPairAddresses(events), + height: block.height, + blockTime: block.time, + reason: "touched", + }); + } + await advanceCursor(client, { cursorId: this.config.cursorId, height: block.height, blockHash: block.hash }); + await client.query("COMMIT"); + } + catch (error) { + await client.query("ROLLBACK"); + throw error; + } + }); + } + async writeReserveSnapshots(events, height, blockTime) { + const touchedPairs = touchedPairAddresses(events); + if (touchedPairs.length === 0) + return; + const knownPairs = await withClient(this.pool, async (client) => { + const result = await client.query(`SELECT pair_address FROM pools WHERE chain_id = $1 AND pair_address = ANY($2::text[])`, [this.config.chainId, touchedPairs]); + return new Set(result.rows.map((row) => row.pair_address)); + }); + for (const pairAddress of touchedPairs) { + if (!knownPairs.has(pairAddress)) + continue; + try { + const state = await this.rest.poolState(pairAddress, height); + await withClient(this.pool, async (client) => upsertPoolStateSnapshot(client, { + chainId: this.config.chainId, + pairAddress, + height, + blockTime, + reserves: state.reserves, + totalShare: state.totalShare, + source: "lcd", + })); + } + catch (error) { + console.warn("indexer_reserve_snapshot_failed", { pairAddress, height, error: error instanceof Error ? error.message : String(error) }); + } + } + } +} +function touchedPairAddresses(events) { + return Array.from(new Set(events + .filter(isPairStateEvent) + .map((event) => event.pairAddress) + .filter(Boolean))); +} +function isPairStateEvent(event) { + return event.kind === "swap" || event.kind === "provide" || event.kind === "withdraw"; +} +function countRangeEvents(blocks) { + const counts = {}; + for (const { events } of blocks) { + for (const event of events) + counts[event.kind] = (counts[event.kind] ?? 0) + 1; + } + return counts; +} +function isReorgHaltError(error) { + const message = error instanceof Error ? error.message : String(error); + return /processed block (hash mismatch|parent hash mismatch|conflict)/.test(message); +} +async function withClient(pool, fn) { + const client = await pool.connect(); + try { + return await fn(client); + } + finally { + client.release(); + } +} diff --git a/indexer/dist/src/metrics.js b/indexer/dist/src/metrics.js new file mode 100644 index 000000000..3f1412201 --- /dev/null +++ b/indexer/dist/src/metrics.js @@ -0,0 +1,57 @@ +export class IndexerMetrics { + startedAtMs = Date.now(); + fetchBlocksTotal = 0; + rpcRequestsInFlight = 0; + rpcErrors = new Map(); + decodeBlocksTotal = 0; + writerBlocksTotal = 0; + writerCommitSeconds = null; + writerEvents = new Map(); + reorgHalt = 0; + recordFetchBlock() { + this.fetchBlocksTotal += 1; + } + beginRpcRequest() { + this.rpcRequestsInFlight += 1; + } + endRpcRequest() { + this.rpcRequestsInFlight = Math.max(0, this.rpcRequestsInFlight - 1); + } + recordRpcError(status) { + const key = String(status || "unknown"); + this.rpcErrors.set(key, (this.rpcErrors.get(key) ?? 0) + 1); + } + recordDecodedBlock() { + this.decodeBlocksTotal += 1; + } + recordWriterBlock(commitSeconds) { + this.writerBlocksTotal += 1; + this.writerCommitSeconds = commitSeconds; + } + recordWriterEvents(counts) { + for (const [kind, value] of Object.entries(counts)) { + if (!value) + continue; + this.writerEvents.set(kind, (this.writerEvents.get(kind) ?? 0) + value); + } + } + setReorgHalt(halted) { + this.reorgHalt = halted ? 1 : 0; + } + snapshot() { + const elapsedSeconds = Math.max((Date.now() - this.startedAtMs) / 1000, 0); + const fetchBlocksPerSecond = elapsedSeconds > 0 ? this.fetchBlocksTotal / elapsedSeconds : 0; + return { + fetchBlocksTotal: this.fetchBlocksTotal, + fetchBlocksPerSecond, + rpcRequestsInFlight: this.rpcRequestsInFlight, + rpcErrors: new Map(this.rpcErrors), + decodeBlocksTotal: this.decodeBlocksTotal, + writerBlocksTotal: this.writerBlocksTotal, + writerCommitSeconds: this.writerCommitSeconds, + writerEvents: new Map(this.writerEvents), + reorgHalt: this.reorgHalt, + }; + } +} +export const indexerMetrics = new IndexerMetrics(); diff --git a/indexer/dist/src/migrate.js b/indexer/dist/src/migrate.js new file mode 100644 index 000000000..c4a9765c3 --- /dev/null +++ b/indexer/dist/src/migrate.js @@ -0,0 +1,11 @@ +import { loadConfig } from "./config.js"; +import { createPool, runMigrations } from "./db.js"; +const config = loadConfig(); +const pool = createPool(config); +try { + const applied = await runMigrations(pool); + console.log(`migrations checked: ${applied.join(", ")}`); +} +finally { + await pool.end(); +} diff --git a/indexer/dist/src/openapi.js b/indexer/dist/src/openapi.js new file mode 100644 index 000000000..fbdaf29e7 --- /dev/null +++ b/indexer/dist/src/openapi.js @@ -0,0 +1,158 @@ +const errorResponse = { + type: "object", + properties: { + error: { type: "string" }, + message: { type: "string" }, + }, + required: ["error"], +}; +const pagination = { + type: "object", + properties: { + limit: { type: "integer", minimum: 1, maximum: 500 }, + nextCursor: { type: ["string", "null"] }, + }, + required: ["limit", "nextCursor"], +}; +const assetAmount = { + type: "object", + properties: { + denom: { type: "string" }, + reserve: { type: ["string", "null"], description: "Pool reserve amount from latest persisted pool_state_snapshots row." }, + amount: { type: "string" }, + valueUsd: { type: ["number", "null"] }, + valueJuno: { type: ["number", "null"] }, + priceUsd: { type: ["number", "null"] }, + priceJuno: { type: ["number", "null"] }, + priceStatus: { type: "string", enum: ["fresh", "stale", "missing"] }, + }, +}; +const pool = { + type: "object", + properties: { + id: { type: "string" }, + pair: { type: "string" }, + pairAddress: { type: "string" }, + lpToken: { type: ["string", "null"] }, + poolType: { type: ["string", "null"] }, + assets: { type: "array", items: assetAmount }, + totalShare: { type: ["string", "null"], description: "Latest total LP share from persisted pool_state_snapshots." }, + tvlUsd: { type: ["number", "null"], description: "Null when valuation is unavailable; never fabricated as zero." }, + tvlJuno: { type: ["number", "null"] }, + volume24hUsd: { type: ["number", "null"] }, + volume24hJuno: { type: ["number", "null"] }, + volume7dUsd: { type: ["number", "null"] }, + volume7dJuno: { type: ["number", "null"] }, + fees24hUsd: { type: ["number", "null"] }, + fees24hJuno: { type: ["number", "null"] }, + feeBps: { type: ["number", "null"] }, + feeApr: { type: "number" }, + incentivesApr: { type: "number" }, + totalApr: { type: "number" }, + incentivized: { type: "boolean" }, + updatedAt: { type: "string", format: "date-time" }, + dataSource: { type: "string", const: "indexer" }, + isMock: { type: "boolean", const: false }, + }, + required: ["id", "pairAddress", "assets", "dataSource", "isMock"], +}; +const price = { + type: "object", + properties: { + asset: { type: "string" }, + priceUsd: { type: ["number", "null"] }, + priceJuno: { type: ["number", "null"] }, + source: { type: ["string", "null"] }, + status: { type: "string", enum: ["fresh", "stale", "missing"] }, + stale: { type: "boolean" }, + observedAt: { type: ["string", "null"], format: "date-time" }, + ageMs: { type: ["integer", "null"] }, + isMock: { type: "boolean", const: false }, + }, + required: ["asset", "priceUsd", "priceJuno", "status", "stale", "isMock"], +}; +const candle = { + type: "object", + properties: { + poolId: { type: "string" }, + pairAddress: { type: "string" }, + baseAsset: { type: "string" }, + quoteAsset: { type: "string" }, + interval: { type: "string", enum: ["5m", "1h", "1d"] }, + bucketStart: { type: "string", format: "date-time" }, + open: { type: ["number", "null"] }, + high: { type: ["number", "null"] }, + low: { type: ["number", "null"] }, + close: { type: ["number", "null"] }, + volume: { type: ["number", "null"] }, + volumeQuote: { type: ["number", "null"], description: "Quote-asset volume, stored separately from USD volume." }, + tradeCount: { type: "integer" }, + dataSource: { type: "string", const: "indexer" }, + isMock: { type: "boolean", const: false }, + }, +}; +const walletTransaction = { + type: "object", + properties: { + txHash: { type: "string" }, + walletAddress: { type: ["string", "null"] }, + poolId: { type: ["string", "null"] }, + pairAddress: { type: ["string", "null"] }, + type: { type: "string" }, + height: { type: "integer" }, + timestamp: { type: "string", format: "date-time" }, + offerAsset: { type: ["object", "null"] }, + askAsset: { type: ["object", "null"] }, + amountUsd: { type: ["number", "null"] }, + feeUsd: { type: ["number", "null"] }, + success: { type: "boolean" }, + dataSource: { type: "string", const: "indexer" }, + isMock: { type: "boolean", const: false }, + }, +}; +const limitParam = { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 500 }, required: false }; +const cursorParam = { name: "cursor", in: "query", schema: { type: "string" }, required: false }; +const assetQueryParam = { name: "assets", in: "query", schema: { type: "string" }, required: false, description: "Comma-separated native denoms, IBC denoms, or CW20 contract addresses." }; +const assetPathParam = { name: "asset", in: "path", schema: { type: "string" }, required: true }; +const idPathParam = { name: "id", in: "path", schema: { type: "string" }, required: true, description: "Pool UUID or pair address." }; +const walletPathParam = { name: "addr", in: "path", schema: { type: "string" }, required: true, description: "Juno wallet address." }; +const candleQueryParams = [ + { name: "interval", in: "query", schema: { type: "string", enum: ["5m", "1h", "1d"] }, required: false }, + { name: "from", in: "query", schema: { type: "string", format: "date-time" }, required: false }, + { name: "to", in: "query", schema: { type: "string", format: "date-time" }, required: false }, + { name: "baseAsset", in: "query", schema: { type: "string" }, required: false }, + { name: "quoteAsset", in: "query", schema: { type: "string" }, required: false }, +]; +function ok(schema, extra = {}) { + return { + ...extra, + responses: { + "200": { description: "OK", content: { "application/json": { schema } } }, + "400": { description: "Bad request", content: { "application/json": { schema: errorResponse } } }, + "404": { description: "Not found", content: { "application/json": { schema: errorResponse } } }, + "503": { description: "Not ready", content: { "application/json": { schema } } }, + "500": { description: "Internal error", content: { "application/json": { schema: errorResponse } } }, + }, + }; +} +export const openApiDocument = { + openapi: "3.1.0", + info: { title: "Astroport Juno Production Indexer API", version: "0.1.0" }, + servers: [{ url: "/" }], + paths: { + "/health": { get: ok({ type: "object", properties: { status: { type: "string", const: "ok" }, service: { type: "string" }, chainId: { type: "string" }, confirmationDepth: { type: "number" }, cursorHeight: { type: ["number", "null"] }, cursorAgeMs: { type: ["number", "null"] }, headHeight: { type: ["number", "null"] }, confirmedTargetHeight: { type: ["number", "null"] }, lag: { type: ["number", "null"] }, confirmedLag: { type: ["number", "null"] }, rpcConfigured: { type: "boolean" }, rpcReachable: { type: "boolean" }, dataSource: { type: "string", const: "indexer" }, isMock: { type: "boolean", const: false } } }) }, + "/ready": { get: ok({ type: "object", properties: { status: { type: "string", enum: ["ready", "not_ready"] }, checks: { type: "object", properties: { database: { type: "boolean" }, migrations: { type: "boolean" }, rpc: { type: "boolean" } } }, migrationsApplied: { type: "integer" }, expectedMigrations: { type: ["integer", "null"] }, missingMigrations: { type: "array", items: { type: "string" } }, rpcConfigured: { type: "boolean" }, rpcReachable: { type: "boolean" }, dataSource: { type: "string", const: "indexer" }, isMock: { type: "boolean", const: false } } }) }, + "/openapi.json": { get: ok({ type: "object" }) }, + "/metrics": { get: { responses: { "200": { description: "Prometheus text exposition metrics for indexer readiness, lag, cursor, RPC, and migration status.", content: { "text/plain": { schema: { type: "string" } } } }, "500": { description: "Internal error", content: { "application/json": { schema: errorResponse } } } } } }, + "/stats": { get: ok({ type: "object", properties: { poolCount: { type: "integer" }, tvlUsd: { type: ["number", "null"] }, tvlJuno: { type: ["number", "null"] }, volume24hUsd: { type: ["number", "null"] }, volume24hJuno: { type: ["number", "null"] }, volume7dUsd: { type: ["number", "null"] }, volume7dJuno: { type: ["number", "null"] }, fees24hUsd: { type: ["number", "null"] }, fees24hJuno: { type: ["number", "null"] }, incentivizedPools: { type: "integer" }, updatedAt: { type: "string", format: "date-time" }, dataSource: { type: "string", const: "indexer" }, isMock: { type: "boolean", const: false } } }) }, + "/prices": { get: ok({ type: "object", properties: { data: { type: "array", items: price } }, required: ["data"] }, { parameters: [assetQueryParam] }) }, + "/prices/{asset}": { get: ok(price, { parameters: [assetPathParam] }) }, + "/pools": { get: ok({ type: "object", properties: { data: { type: "array", items: pool }, pagination }, required: ["data", "pagination"] }, { parameters: [limitParam, cursorParam, { name: "pair", in: "query", schema: { type: "string" }, required: false }] }) }, + "/pools/{id}": { get: ok(pool, { parameters: [idPathParam] }) }, + "/pools/{id}/candles": { get: ok({ type: "object", properties: { data: { type: "array", items: candle }, pagination, meta: { type: "object" } }, required: ["data", "pagination", "meta"] }, { parameters: [idPathParam, limitParam, cursorParam, ...candleQueryParams] }) }, + "/pools/{id}/positions": { get: ok({ type: "object", properties: { data: { type: "array" }, pagination }, required: ["data", "pagination"] }, { parameters: [idPathParam, limitParam, cursorParam] }) }, + "/pools/{id}/history": { get: ok({ type: "object", properties: { data: { type: "array", items: walletTransaction }, pagination }, required: ["data", "pagination"] }, { parameters: [idPathParam, limitParam, cursorParam] }) }, + "/wallets/{addr}/positions": { get: ok({ type: "object", properties: { data: { type: "array" }, pagination }, required: ["data", "pagination"] }, { parameters: [walletPathParam, limitParam, cursorParam] }) }, + "/wallets/{addr}/history": { get: ok({ type: "object", properties: { data: { type: "array", items: walletTransaction }, pagination }, required: ["data", "pagination"] }, { parameters: [walletPathParam, limitParam, cursorParam] }) }, + }, +}; diff --git a/indexer/dist/src/ranges.js b/indexer/dist/src/ranges.js new file mode 100644 index 000000000..31d49d14f --- /dev/null +++ b/indexer/dist/src/ranges.js @@ -0,0 +1,15 @@ +export function parseNonNegativeInteger(value, name) { + if (!/^\d+$/.test(value)) + throw new Error(`${name} must be a non-negative integer`); + const parsed = Number(value); + if (!Number.isSafeInteger(parsed)) + throw new Error(`${name} must be a non-negative integer`); + return parsed; +} +export function nextBlockRange(input) { + const from = input.lastHeight + 1; + const batchTo = input.lastHeight + Math.max(1, input.batchSize); + const cappedTarget = input.maxHeight === undefined ? input.confirmedTarget : Math.min(input.confirmedTarget, input.maxHeight); + const to = Math.min(cappedTarget, batchTo); + return { from, to, empty: to < from }; +} diff --git a/indexer/dist/src/read-model-refresher.js b/indexer/dist/src/read-model-refresher.js new file mode 100644 index 000000000..19a5956c8 --- /dev/null +++ b/indexer/dist/src/read-model-refresher.js @@ -0,0 +1,45 @@ +import { refreshApiReadModels } from "./db.js"; +export class ReadModelRefresher { + pool; + options; + timer; + running = false; + constructor(pool, options) { + this.pool = pool; + this.options = options; + } + async refreshOnce() { + if (this.running) + return; + this.running = true; + const client = await this.pool.connect(); + try { + const results = await refreshApiReadModels(client, { chainId: this.options.chainId }); + console.log(JSON.stringify({ + msg: "indexer_read_models_refreshed", + role: "indexer", + models: results, + })); + } + finally { + client.release(); + this.running = false; + } + } + start() { + if (this.options.intervalMs <= 0 || this.timer) + return; + this.timer = setInterval(() => { + this.refreshOnce().catch((error) => { + console.warn("indexer_read_models_refresh_failed", { error: error instanceof Error ? error.message : String(error) }); + }); + }, this.options.intervalMs); + this.timer.unref(); + } + stop() { + if (!this.timer) + return; + clearInterval(this.timer); + this.timer = undefined; + } +} diff --git a/indexer/dist/src/refresh-read-models.js b/indexer/dist/src/refresh-read-models.js new file mode 100644 index 000000000..5f9a8584e --- /dev/null +++ b/indexer/dist/src/refresh-read-models.js @@ -0,0 +1,15 @@ +import { loadConfig } from "./config.js"; +import { createPool, refreshApiReadModels } from "./db.js"; +const config = loadConfig(); +const pool = createPool(config); +const client = await pool.connect(); +try { + const results = await refreshApiReadModels(client, { chainId: config.chainId }); + for (const result of results) { + console.log(`read_model_refreshed model=${result.model} rows=${result.rowsAffected}`); + } +} +finally { + client.release(); + await pool.end(); +} diff --git a/indexer/dist/src/rpc.js b/indexer/dist/src/rpc.js new file mode 100644 index 000000000..4e4f20721 --- /dev/null +++ b/indexer/dist/src/rpc.js @@ -0,0 +1,176 @@ +import { createHash } from "node:crypto"; +export class JunoRestClient { + restUrl; + timeoutMs; + maxRetries; + constructor(restUrl, timeoutMs = 5_000, maxRetries = 2) { + this.restUrl = restUrl; + this.timeoutMs = timeoutMs; + this.maxRetries = maxRetries; + } + async poolState(pairAddress, height) { + const encodedQuery = encodeURIComponent(Buffer.from(JSON.stringify({ pool: {} })).toString("base64")); + const headers = {}; + if (height !== undefined) + headers["x-cosmos-block-height"] = String(height); + const path = `/cosmwasm/wasm/v1/contract/${pairAddress}/smart/${encodedQuery}`; + const json = await this.getJson(path, headers); + return normalizePoolState(json.data ?? json); + } + async getJson(path, headers) { + let lastError; + for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await fetch(`${this.restUrl}${path}`, { headers, signal: controller.signal }); + if (response.ok) + return await response.json(); + if (!isTransientStatus(response.status) || attempt === this.maxRetries) { + throw new Error(`LCD smart query failed: ${response.status} ${response.statusText}`); + } + lastError = new Error(`LCD smart query failed: ${response.status} ${response.statusText}`); + } + catch (error) { + lastError = error; + if (attempt === this.maxRetries || !isTransientFetchError(error)) + throw error; + } + finally { + clearTimeout(timeout); + } + await delay(100 * 2 ** attempt); + } + throw lastError instanceof Error ? lastError : new Error("LCD smart query failed"); + } +} +export class JunoRpcClient { + rpcUrl; + metrics; + timeoutMs; + maxRetries; + constructor(rpcUrl, options = {}) { + this.rpcUrl = rpcUrl; + this.metrics = options.metrics; + this.timeoutMs = options.timeoutMs ?? 10_000; + this.maxRetries = options.maxRetries ?? 5; + } + async head() { + const json = await this.get("/status"); + const latest = json.result.sync_info; + return { height: Number(latest.latest_block_height), hash: String(latest.latest_block_hash) }; + } + async block(height) { + const [blockJson, resultsJson] = await Promise.all([ + this.get(`/block?height=${height}`), + this.get(`/block_results?height=${height}`), + ]); + const block = blockJson.result.block; + const header = block.header; + const data = block.data; + const results = resultsJson.result; + const txsResults = (results.txs_results ?? []); + const txs = (data.txs ?? []); + const bundle = { + height, + hash: String(blockJson.result.block_id ? blockJson.result.block_id.hash : header.last_block_id ?? ""), + parentHash: String(((header.last_block_id?.hash) ?? "")) || undefined, + time: String(header.time), + txCount: txs.length, + txEvents: txsResults.map((tx, index) => ({ + txHash: String(tx.hash ?? txHashFromBase64(txs[index]) ?? `height-${height}-tx-${index}`), + events: convertEvents((tx.events ?? [])), + })), + }; + this.metrics?.recordFetchBlock(); + return bundle; + } + async get(path) { + let lastError; + for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + this.metrics?.beginRpcRequest(); + try { + const response = await fetch(`${this.rpcUrl}${path}`, { signal: controller.signal }); + if (response.ok) + return await response.json(); + const error = new Error(`RPC ${path} failed: ${response.status} ${response.statusText}`); + this.metrics?.recordRpcError(response.status); + if (!isTransientStatus(response.status) || attempt === this.maxRetries) + throw error; + lastError = error; + } + catch (error) { + lastError = error; + if (!(error instanceof Error && error.message.startsWith(`RPC ${path} failed:`))) + this.metrics?.recordRpcError("network"); + if (attempt === this.maxRetries || !isTransientFetchError(error)) + throw error; + } + finally { + clearTimeout(timeout); + this.metrics?.endRpcRequest(); + } + await delay(100 * 2 ** attempt); + } + throw lastError instanceof Error ? lastError : new Error(`RPC ${path} failed`); + } +} +function isTransientStatus(status) { + return status === 408 || status === 425 || status === 429 || status >= 500; +} +function isTransientFetchError(error) { + return error instanceof Error && (error.name === "AbortError" || error.name === "TypeError"); +} +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +function normalizePoolState(value) { + if (!value || typeof value !== "object") + throw new Error("LCD pool query returned non-object data"); + const raw = value; + const assets = Array.isArray(raw.assets) ? raw.assets : []; + const reserves = assets.map(normalizePoolAsset).filter((asset) => asset !== null); + if (reserves.length === 0) + throw new Error("LCD pool query returned no reserves"); + return { reserves, totalShare: raw.total_share === null || raw.total_share === undefined ? null : String(raw.total_share) }; +} +function normalizePoolAsset(value) { + if (!value || typeof value !== "object") + return null; + const raw = value; + const denom = normalizeAssetInfo(raw.info ?? raw.asset_info ?? raw.denom ?? raw.asset); + if (!denom || raw.amount === null || raw.amount === undefined) + return null; + return { denom, amount: String(raw.amount) }; +} +function normalizeAssetInfo(value) { + if (typeof value === "string") + return value; + if (!value || typeof value !== "object") + return ""; + const raw = value; + const native = raw.native_token; + if (native && typeof native === "object") + return String(native.denom ?? ""); + const token = raw.token; + if (token && typeof token === "object") + return String(token.contract_addr ?? ""); + return String(raw.denom ?? raw.asset ?? ""); +} +function txHashFromBase64(tx) { + if (!tx) + return undefined; + return createHash("sha256").update(Buffer.from(tx, "base64")).digest("hex").toUpperCase(); +} +function convertEvents(events) { + return events.map((event) => ({ + type: String(event.type), + attributes: (event.attributes ?? []).map((attribute) => ({ + key: String(attribute.key), + value: String(attribute.value), + index: Boolean(attribute.index), + })), + })); +} diff --git a/indexer/dist/src/seed-asset-metadata.js b/indexer/dist/src/seed-asset-metadata.js new file mode 100644 index 000000000..90ab147a1 --- /dev/null +++ b/indexer/dist/src/seed-asset-metadata.js @@ -0,0 +1,60 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { loadConfig } from "./config.js"; +import { createPool } from "./db.js"; +const args = new Map(); +for (const arg of process.argv.slice(2)) { + const [key, value = ""] = arg.replace(/^--/, "").split("="); + if (key) + args.set(key, value); +} +function isRegistryAsset(value) { + return typeof value === "object" && value !== null; +} +function collectAssets(registry) { + const assets = new Map(); + const pools = Array.isArray(registry.pools) ? registry.pools : []; + for (const pool of pools) { + const poolAssets = Array.isArray(pool.assets) ? pool.assets : []; + for (const asset of poolAssets) { + if (!isRegistryAsset(asset) || typeof asset.id !== "string") + continue; + if (!Number.isInteger(asset.decimals) || Number(asset.decimals) < 0 || Number(asset.decimals) > 36) + continue; + assets.set(asset.id, { + symbol: typeof asset.symbol === "string" ? asset.symbol : null, + decimals: Number(asset.decimals), + logoUri: typeof asset.logoURI === "string" ? asset.logoURI : null, + verified: asset.verified === true, + }); + } + } + return assets; +} +const config = loadConfig(); +const registryPath = resolve(args.get("registry") ?? "../../frontend/src/data/registry.juno-1.json"); +const registry = JSON.parse(await readFile(registryPath, "utf8")); +const chainId = args.get("chain-id") ?? (typeof registry.chainId === "string" ? registry.chainId : config.chainId); +const assets = collectAssets(registry); +const pool = createPool(config); +const client = await pool.connect(); +try { + let upserted = 0; + for (const [asset, metadata] of assets) { + const result = await client.query(`INSERT INTO asset_metadata(chain_id, asset, symbol, decimals, logo_uri, verified, source) + VALUES ($1,$2,$3,$4,$5,$6,'registry') + ON CONFLICT (chain_id, asset) DO UPDATE + SET symbol = EXCLUDED.symbol, + decimals = EXCLUDED.decimals, + logo_uri = EXCLUDED.logo_uri, + verified = EXCLUDED.verified, + source = EXCLUDED.source, + updated_at = now()`, [chainId, asset, metadata.symbol, metadata.decimals, metadata.logoUri, metadata.verified]); + upserted += result.rowCount ?? 0; + } + console.log(`asset_metadata_seeded chain_id=${chainId} assets=${assets.size} rows=${upserted} registry=${registryPath}`); +} +finally { + client.release(); + await pool.end(); +} diff --git a/indexer/dist/src/snapshot-worker.js b/indexer/dist/src/snapshot-worker.js new file mode 100644 index 000000000..e17fef802 --- /dev/null +++ b/indexer/dist/src/snapshot-worker.js @@ -0,0 +1,119 @@ +import { loadConfig } from "./config.js"; +import { claimSnapshotJobs, createPool, markSnapshotJobFailed, markSnapshotJobSucceeded, upsertPoolStateSnapshot, } from "./db.js"; +import { JunoRestClient } from "./rpc.js"; +const DEFAULT_BATCH_SIZE = 25; +const DEFAULT_LEASE_SECONDS = 60; +const DEFAULT_MAX_ATTEMPTS = 5; +export class SnapshotWorker { + config; + pool; + ownsPool; + rest; + batchSize; + leaseSeconds; + maxAttempts; + pollIntervalMs; + constructor(config, pool, rest, options = {}) { + this.config = config; + this.pool = pool ?? createPool(config); + this.ownsPool = !pool; + this.rest = rest ?? new JunoRestClient(config.restUrl); + this.batchSize = options.batchSize ?? intEnv("SNAPSHOT_WORKER_BATCH_SIZE", DEFAULT_BATCH_SIZE); + this.leaseSeconds = options.leaseSeconds ?? intEnv("SNAPSHOT_JOB_LEASE_SECONDS", DEFAULT_LEASE_SECONDS); + this.maxAttempts = options.maxAttempts ?? intEnv("SNAPSHOT_JOB_MAX_ATTEMPTS", DEFAULT_MAX_ATTEMPTS); + this.pollIntervalMs = options.pollIntervalMs ?? config.pollIntervalMs; + } + async close() { + if (this.ownsPool) + await this.pool?.end(); + } + async processBatch() { + const jobs = await withClient(this.pool, (client) => claimSnapshotJobs(client, { + chainId: this.config.chainId, + limit: this.batchSize, + leaseSeconds: this.leaseSeconds, + maxAttempts: this.maxAttempts, + })); + for (const job of jobs) + await this.processJob(job); + return jobs.length; + } + async runForever() { + for (;;) { + const processed = await this.processBatch(); + console.log(`snapshot worker processed=${processed}`); + await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs)); + } + } + async processJob(job) { + try { + const state = await this.rest.poolState(job.pairAddress, job.height); + await withClient(this.pool, async (client) => { + await client.query("BEGIN"); + try { + await upsertPoolStateSnapshot(client, { + chainId: job.chainId, + pairAddress: job.pairAddress, + height: job.height, + blockTime: job.blockTime, + reserves: state.reserves, + totalShare: state.totalShare, + source: "lcd", + }); + await markSnapshotJobSucceeded(client, { jobId: job.id, attempt: job.attempts }); + await client.query("COMMIT"); + } + catch (error) { + await client.query("ROLLBACK"); + throw error; + } + }); + } + catch (error) { + const message = error instanceof Error ? error.message : String(error); + await withClient(this.pool, (client) => markSnapshotJobFailed(client, { + jobId: job.id, + attempt: job.attempts, + error: message, + permanent: isPermanentSnapshotFailure(error), + maxAttempts: this.maxAttempts, + })); + } + } +} +export function isPermanentSnapshotFailure(error) { + const message = error instanceof Error ? error.message : String(error); + const status = message.match(/LCD smart query failed: (\d{3})\b/)?.[1]; + if (status) { + const code = Number(status); + return code >= 400 && code < 500 && ![408, 425, 429].includes(code); + } + return /no reserves|non-object data|unknown pair/i.test(message); +} +function intEnv(name, fallback) { + const value = process.env[name]; + if (!value) + return fallback; + const parsed = Number.parseInt(value, 10); + if (!Number.isSafeInteger(parsed) || parsed < 1) + throw new Error(`${name} must be an integer greater than or equal to 1`); + return parsed; +} +async function withClient(pool, fn) { + const client = await pool.connect(); + try { + return await fn(client); + } + finally { + client.release(); + } +} +if (import.meta.url === `file://${process.argv[1]}`) { + const worker = new SnapshotWorker(loadConfig()); + try { + await worker.runForever(); + } + finally { + await worker.close(); + } +} diff --git a/indexer/dist/test/api.test.js b/indexer/dist/test/api.test.js new file mode 100644 index 000000000..bf9f63adc --- /dev/null +++ b/indexer/dist/test/api.test.js @@ -0,0 +1,288 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createIndexerApi } from "../src/api.js"; +import { PostgresApiStore } from "../src/api-store.js"; +import { IndexerMetrics } from "../src/metrics.js"; +class FakeDb { + calls = []; + async query(text, values) { + this.calls.push({ text, values }); + if (text === "SELECT 1") + return { rows: [{ "?column?": 1 }] }; + if (text.includes("FROM schema_migrations")) + return { rows: [{ version: "001_init.sql" }, { version: "002_pool_candles.sql" }, { version: "003_api_pricing_readiness.sql" }] }; + if (text.includes("FROM indexer_cursors")) + return { rows: [{ last_height: "42", updated_at: "2026-07-03T00:00:00.000Z" }] }; + if (text.includes("FROM protocol_stats_latest")) + return { rows: [{ pool_count: 1, incentivized_pools: 1, updated_at: "2026-07-03T00:00:00.000Z", tvl_usd: null, tvl_juno: "1000", volume_24h_usd: null, volume_24h_juno: "25", volume_7d_usd: null, volume_7d_juno: "100", fees_24h_usd: null, fees_24h_juno: "0.3" }] }; + if (text.includes("FROM token_prices")) + return { rows: [{ asset: "ujuno", price_usd: null, price_juno: "1", source: "pool", status: "fresh", observed_at: "2026-07-03T00:00:00.000Z" }] }; + if (text.includes("FROM latest_pool_state") && text.includes("LIMIT 1")) { + return { rows: [poolRow()] }; + } + if (text.includes("FROM latest_pool_state")) + return { rows: [poolRow()] }; + if (text.includes("FROM pools p") && text.includes("LIMIT 1")) + return { rows: [poolRow()] }; + if (text.includes("FROM pools p")) + return { rows: [poolRow()] }; + if (text.includes("FROM pool_candle_buckets")) { + return { rows: [{ pool_id: "pool-1", pair_address: "juno1pair", asset: "ujuno", quote_asset: "uusdc", interval: "1h", bucket_start: "2026-07-03T00:00:00.000Z", open: "1", high: "1.2", low: "0.9", close: "1.1", volume: "10", volume_quote: "11", trade_count: 2 }] }; + } + if (text.includes("FROM wallet_position_latest")) + return { rows: [{ wallet_address: "juno1wallet", owner_address: "juno1wallet", pool_id: "pool-1", pair_address: "juno1pair", lp_token_address: "factory/juno1pair/astroport/share", lp_balance: "7", bonded_balance: "2", total_share: "789", tvl_usd: null, tvl_juno: "1000", asset_infos: [{ native_token: { denom: "ujuno" } }, { native_token: { denom: "uusdc" } }], reserves: [{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }], updated_at: "2026-07-03T00:00:00.000Z" }] }; + if (text.includes("FROM wallet_history_flat")) + return { rows: [{ tx_hash: "tx-1", wallet_address: "juno1wallet", pair_address: "juno1pair", type: "swap", height: "42", timestamp: "2026-07-03T00:00:00.000Z", offer_asset: { denom: "ujuno", amount: "1" }, ask_asset: { denom: "uusdc", amount: "2" }, amount_usd: null, fee_usd: null, success: true }] }; + throw new Error(`unexpected query: ${text}`); + } +} +class EmptyReadModelDb { + calls = []; + async query(text, values) { + this.calls.push({ text, values }); + if (text === "SELECT 1") + return { rows: [{ "?column?": 1 }] }; + if (text.includes("FROM schema_migrations")) + return { rows: [{ version: "001_init.sql" }, { version: "002_pool_candles.sql" }, { version: "003_api_pricing_readiness.sql" }] }; + if (text.includes("FROM indexer_cursors")) + return { rows: [] }; + if (text.includes("FROM protocol_stats_latest")) + return { rows: [] }; + if (text.includes("FROM latest_pool_state")) + return { rows: [] }; + if (text.includes("FROM pools p")) + return { rows: [] }; + if (text.includes("FROM pool_candle_buckets")) + return { rows: [] }; + if (text.includes("FROM wallet_position_latest")) + return { rows: [] }; + if (text.includes("FROM wallet_history_flat")) + return { rows: [] }; + throw new Error(`unexpected query: ${text}`); + } +} +class CandleFilterFallbackDb extends FakeDb { + async query(text, values) { + this.calls.push({ text, values }); + if (text.includes("FROM pool_candle_buckets")) { + if (values?.[3] || values?.[4]) + return { rows: [] }; + return { rows: [{ pool_id: "pool-1", pair_address: "juno1pair", asset: "ujuno", quote_asset: "uusdc", interval: "5m", bucket_start: "2026-07-03T00:00:00.000Z", open: "1", high: "1.2", low: "0.9", close: "1.1", volume: "10", volume_quote: "11", trade_count: 2 }] }; + } + this.calls.pop(); + return super.query(text, values); + } +} +function poolRow() { + return { + id: "pool-1", + chain_id: "juno-1", + pair_address: "juno1pair", + liquidity_token_address: "factory/juno1pair/astroport/share", + pool_type: "xyk", + asset_infos: [{ native_token: { denom: "ujuno" } }, { native_token: { denom: "uusdc" } }], + tvl_usd: null, + tvl_juno: "1000", + total_share: "789", + reserves: [{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }], + updated_at: "2026-07-03T00:00:00.000Z", + }; +} +async function start(db = new FakeDb()) { + const store = new PostgresApiStore(db, "juno-1", "cursor"); + const server = createIndexerApi(store); + await new Promise((resolve) => server.listen(0, resolve)); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing port"); + return { db, server, baseUrl: `http://127.0.0.1:${address.port}` }; +} +let openServer; +afterEach(async () => { + vi.restoreAllMocks(); + if (openServer) + await new Promise((resolve, reject) => openServer.close((error) => (error ? reject(error) : resolve()))); + openServer = undefined; +}); +describe("production API", () => { + it("serves health, readiness, stats and OpenAPI without mock markers", async () => { + const { server, baseUrl } = await start(); + openServer = server; + const health = await (await fetch(`${baseUrl}/health`)).json(); + expect(health).toMatchObject({ status: "ok", service: "astroport-juno-indexer", dataSource: "indexer", isMock: false, confirmationDepth: 0, cursorHeight: 42, confirmedTargetHeight: null, confirmedLag: null, rpcConfigured: false, rpcReachable: false }); + const ready = await (await fetch(`${baseUrl}/ready`)).json(); + expect(ready).toMatchObject({ status: "ready", database: "ok", migrationsApplied: 3, checks: { database: true, migrations: true, rpc: true } }); + const stats = await (await fetch(`${baseUrl}/stats`)).json(); + expect(stats).toMatchObject({ poolCount: 1, tvlUsd: null, tvlJuno: 1000, volume24hUsd: null, volume24hJuno: 25, incentivizedPools: 1, isMock: false }); + const openapi = await (await fetch(`${baseUrl}/openapi.json`)).json(); + expect(openapi.paths["/ready"]).toBeTruthy(); + expect(openapi.paths["/metrics"]).toBeTruthy(); + }); + it("serves Prometheus metrics for readiness, cursor, RPC, and migrations", async () => { + const { server, baseUrl } = await start(); + openServer = server; + const response = await fetch(`${baseUrl}/metrics`); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/plain"); + expect(response.headers.get("cache-control")).toContain("no-store"); + const body = await response.text(); + expect(body).toContain("# HELP juno_indexer_ready"); + expect(body).toContain('juno_indexer_ready{chain_id="juno-1"} 1'); + expect(body).toContain('juno_indexer_rpc_configured{chain_id="juno-1"} 0'); + expect(body).toContain('juno_indexer_rpc_reachable{chain_id="juno-1"} 0'); + expect(body).toContain('juno_indexer_cursor_height{chain_id="juno-1"} 42'); + expect(body).toContain('juno_indexer_cursor_age_ms{chain_id="juno-1"}'); + expect(body).toContain('juno_indexer_migrations_applied{chain_id="juno-1"} 3'); + }); + it("serves in-process ingestion throughput metrics when a collector is attached", async () => { + const store = new PostgresApiStore(new FakeDb(), "juno-1", "cursor"); + const metrics = new IndexerMetrics(); + metrics.recordFetchBlock(); + metrics.recordFetchBlock(); + metrics.beginRpcRequest(); + metrics.recordRpcError(429); + metrics.recordDecodedBlock(); + metrics.recordWriterBlock(0.125); + metrics.recordWriterEvents({ swap: 2, provide: 1, incentive: 1 }); + metrics.setReorgHalt(true); + const server = createIndexerApi(store, metrics); + await new Promise((resolve) => server.listen(0, resolve)); + openServer = server; + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing port"); + const body = await (await fetch(`http://127.0.0.1:${address.port}/metrics`)).text(); + expect(body).toContain("# HELP juno_indexer_fetch_blocks_total"); + expect(body).toContain("juno_indexer_fetch_blocks_total 2"); + expect(body).toMatch(/juno_indexer_fetch_blocks_per_second \d/); + expect(body).toContain("juno_indexer_fetch_rpc_requests_in_flight 1"); + expect(body).toContain('juno_indexer_fetch_rpc_error_total{status="429"} 1'); + expect(body).toContain("juno_indexer_decode_blocks_total 1"); + expect(body).toContain("juno_indexer_writer_blocks_total 1"); + expect(body).toContain("juno_indexer_writer_commit_seconds 0.125"); + expect(body).toContain('juno_indexer_writer_events_total{kind="swap"} 2'); + expect(body).toContain('juno_indexer_writer_events_total{kind="provide"} 1'); + expect(body).toContain('juno_indexer_writer_events_total{kind="incentive"} 1'); + expect(body).toContain("juno_indexer_reorg_halt 1"); + }); + it("uses one shared RPC head check per metrics scrape", async () => { + const originalFetch = globalThis.fetch.bind(globalThis); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + if (String(input) === "http://rpc.example/status") { + return { + ok: true, + json: async () => ({ result: { sync_info: { latest_block_height: "50", latest_block_hash: "head-hash" } } }), + }; + } + return originalFetch(input, init); + }); + const store = new PostgresApiStore(new FakeDb(), "juno-1", "cursor", { rpcUrl: "http://rpc.example", confirmationDepth: 2 }); + const server = createIndexerApi(store); + await new Promise((resolve) => server.listen(0, resolve)); + openServer = server; + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing port"); + const body = await (await fetch(`http://127.0.0.1:${address.port}/metrics`)).text(); + const rpcCalls = fetchSpy.mock.calls.filter(([input]) => String(input) === "http://rpc.example/status"); + expect(rpcCalls).toHaveLength(1); + expect(rpcCalls[0]?.[0]).toBe("http://rpc.example/status"); + expect(body).toContain('juno_indexer_rpc_configured{chain_id="juno-1"} 1'); + expect(body).toContain('juno_indexer_rpc_reachable{chain_id="juno-1"} 1'); + expect(body).toContain('juno_indexer_head_height{chain_id="juno-1"} 50'); + expect(body).toContain('juno_indexer_confirmed_target_height{chain_id="juno-1"} 48'); + expect(body).toContain('juno_indexer_confirmed_lag_blocks{chain_id="juno-1"} 6'); + }); + it("returns frontend-compatible pool, price and candle responses from Postgres rows", async () => { + const { db, server, baseUrl } = await start(); + openServer = server; + const pools = await (await fetch(`${baseUrl}/pools`)).json(); + expect(pools.data[0]).toMatchObject({ id: "pool-1", pairAddress: "juno1pair", tvlUsd: null, tvlJuno: 1000, totalShare: "789", isMock: false }); + expect(pools.data[0].assets[0]).toMatchObject({ denom: "ujuno", reserve: "123", priceJuno: null, priceStatus: "missing" }); + expect(pools.data[0].assets[1]).toMatchObject({ denom: "uusdc", reserve: "456" }); + const poolDetail = await (await fetch(`${baseUrl}/pools/juno1pair`)).json(); + expect(poolDetail).toMatchObject({ id: "pool-1", pairAddress: "juno1pair", totalShare: "789", isMock: false }); + expect(poolDetail.assets[0]).toMatchObject({ denom: "ujuno", reserve: "123" }); + expect(poolDetail.assets[1]).toMatchObject({ denom: "uusdc", reserve: "456" }); + const price = await (await fetch(`${baseUrl}/prices/ujuno`)).json(); + expect(price).toMatchObject({ asset: "ujuno", priceUsd: null, priceJuno: 1, source: "pool", status: "fresh", isMock: false }); + const candles = await (await fetch(`${baseUrl}/pools/juno1pair/candles?interval=1h&limit=999`)).json(); + expect(candles.pagination.limit).toBe(500); + expect(candles.meta).toMatchObject({ pairAddress: "juno1pair", dataSource: "indexer", isMock: false }); + expect(candles.data[0]).toMatchObject({ baseAsset: "ujuno", quoteAsset: "uusdc", close: 1.1, volumeQuote: 11 }); + expect(db.calls.some((call) => call.text.includes("FROM latest_pool_state"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM pool_candle_buckets"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM token_candles"))).toBe(false); + }); + it("falls back to canonical pair candles when requested asset filters do not match", async () => { + const { db, server, baseUrl } = await start(new CandleFilterFallbackDb()); + openServer = server; + const candles = await (await fetch(`${baseUrl}/pools/juno1pair/candles?interval=5m&baseAsset=uusdc"eAsset=ujuno&limit=20`)).json(); + expect(candles.data[0]).toMatchObject({ baseAsset: "ujuno", quoteAsset: "uusdc", close: 1.1 }); + expect(candles.meta).toMatchObject({ + pairAddress: "juno1pair", + interval: "5m", + baseAsset: null, + quoteAsset: null, + requestedBaseAsset: "uusdc", + requestedQuoteAsset: "ujuno", + filterFallback: true, + }); + expect(db.calls.filter((call) => call.text.includes("FROM pool_candle_buckets"))).toHaveLength(2); + }); + it("serves wallet history and positions from read models", async () => { + const { db, server, baseUrl } = await start(); + openServer = server; + const history = await (await fetch(`${baseUrl}/wallets/juno1wallet/history`)).json(); + expect(history.data[0]).toMatchObject({ txHash: "tx-1", walletAddress: "juno1wallet", pairAddress: "juno1pair", type: "swap", height: 42, isMock: false }); + expect(history.data[0].offerAsset).toEqual({ denom: "ujuno", amount: "1" }); + const poolHistory = await (await fetch(`${baseUrl}/pools/juno1pair/history?limit=10`)).json(); + expect(poolHistory.data[0]).toMatchObject({ txHash: "tx-1", pairAddress: "juno1pair", type: "swap" }); + expect(poolHistory.pagination.limit).toBe(10); + const positions = await (await fetch(`${baseUrl}/wallets/juno1wallet/positions`)).json(); + expect(positions.data[0]).toMatchObject({ walletAddress: "juno1wallet", poolId: "pool-1", pairAddress: "juno1pair", lpBalance: "7", bondedBalance: "2", shareBps: 114, valueUsd: null }); + expect(positions.data[0].valueJuno).toBeCloseTo((9 / 789) * 1000, 6); + expect(positions.data[0].assets[0]).toMatchObject({ denom: "ujuno", reserve: "123", amount: "1" }); + expect(db.calls.some((call) => call.text.includes("FROM wallet_history_flat"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM wallet_position_latest"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM swaps"))).toBe(false); + expect(db.calls.some((call) => call.text.includes("FROM positions"))).toBe(false); + }); + it("returns honest empty API responses when read models have no production rows", async () => { + const { db, server, baseUrl } = await start(new EmptyReadModelDb()); + openServer = server; + const stats = await (await fetch(`${baseUrl}/stats`)).json(); + expect(stats).toMatchObject({ poolCount: 0, tvlUsd: null, tvlJuno: null, incentivizedPools: 0, isMock: false }); + const pools = await (await fetch(`${baseUrl}/pools`)).json(); + expect(pools).toMatchObject({ data: [], pagination: { limit: 50, nextCursor: null } }); + const history = await (await fetch(`${baseUrl}/wallets/juno1empty/history`)).json(); + expect(history).toMatchObject({ data: [], pagination: { limit: 50, nextCursor: null } }); + const positions = await (await fetch(`${baseUrl}/wallets/juno1empty/positions`)).json(); + expect(positions).toMatchObject({ data: [], pagination: { limit: 50, nextCursor: null } }); + const poolDetail = await fetch(`${baseUrl}/pools/juno1missing`); + expect(poolDetail.status).toBe(404); + expect(db.calls.some((call) => call.text.includes("FROM protocol_stats_latest"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM latest_pool_state"))).toBe(true); + }); + it("returns HTTP 503 when readiness checks report not_ready", async () => { + const db = new FakeDb(); + const store = new PostgresApiStore(db, "juno-1", "cursor", { expectedMigrationVersions: ["001_init.sql", "002_pool_candles.sql", "003_api_pricing_readiness.sql", "004_pool_state_source_precedence.sql"] }); + const server = createIndexerApi(store); + await new Promise((resolve) => server.listen(0, resolve)); + openServer = server; + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing port"); + const response = await fetch(`http://127.0.0.1:${address.port}/ready`); + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ status: "not_ready", checks: { migrations: false }, missingMigrations: ["004_pool_state_source_precedence.sql"] }); + }); + it("returns structured errors without leaking database internals", async () => { + const db = { query: async () => { throw new Error("secret database internals"); } }; + const { server, baseUrl } = await start(db); + openServer = server; + const response = await fetch(`${baseUrl}/stats`); + expect(response.status).toBe(500); + const body = await response.json(); + expect(body).toEqual({ error: "internal_error" }); + }); +}); diff --git a/indexer/dist/test/block-fetcher.test.js b/indexer/dist/test/block-fetcher.test.js new file mode 100644 index 000000000..faf86bad0 --- /dev/null +++ b/indexer/dist/test/block-fetcher.test.js @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { fetchBlockRange } from "../src/block-fetcher.js"; +function bundle(height) { + return { height, hash: `hash-${height}`, time: "2026-01-01T00:00:00Z", txCount: 0, txEvents: [] }; +} +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +describe("fetchBlockRange", () => { + it("caps in-flight block fetches at the requested concurrency", async () => { + let active = 0; + let maxActive = 0; + const rpc = { + async block(height) { + active += 1; + maxActive = Math.max(maxActive, active); + await sleep(5); + active -= 1; + return bundle(height); + }, + }; + const blocks = await fetchBlockRange({ rpc, from: 10, to: 16, concurrency: 3 }); + expect(maxActive).toBe(3); + expect(blocks.map((block) => block.height)).toEqual([10, 11, 12, 13, 14, 15, 16]); + }); + it("returns bundles sorted by ascending height when requests resolve out of order", async () => { + const rpc = { + async block(height) { + await sleep((5 - height) * 5); + return bundle(height); + }, + }; + const blocks = await fetchBlockRange({ rpc, from: 1, to: 4, concurrency: 4 }); + expect(blocks.map((block) => block.height)).toEqual([1, 2, 3, 4]); + }); + it("fails the whole range with the exhausted height when one block fetch fails", async () => { + const rpc = { + async block(height) { + if (height === 3) + throw new Error("RPC /block?height=3 failed: 503 Service Unavailable"); + return bundle(height); + }, + }; + await expect(fetchBlockRange({ rpc, from: 1, to: 5, concurrency: 2 })).rejects.toThrow(/failed to fetch block 3: RPC \/block\?height=3 failed: 503 Service Unavailable/); + }); + it("stops scheduling new heights after a worker fails", async () => { + const calls = []; + const rpc = { + async block(height) { + calls.push(height); + if (height === 1) { + await sleep(20); + return bundle(height); + } + if (height === 2) + throw new Error("boom"); + return bundle(height); + }, + }; + await expect(fetchBlockRange({ rpc, from: 1, to: 5, concurrency: 2 })).rejects.toThrow(/failed to fetch block 2: boom/); + expect(calls).toEqual([1, 2]); + }); + it("rejects invalid concurrency clearly", async () => { + const rpc = { block: async (height) => bundle(height) }; + await expect(fetchBlockRange({ rpc, from: 1, to: 1, concurrency: 0 })).rejects.toThrow(/concurrency/i); + }); +}); diff --git a/indexer/dist/test/candles.test.js b/indexer/dist/test/candles.test.js new file mode 100644 index 000000000..4369faa37 --- /dev/null +++ b/indexer/dist/test/candles.test.js @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { aggregateSwapsToCandles, bucketStartFor, deriveCanonicalSwapPrice } from "../src/candles.js"; +describe("candle helpers", () => { + it("buckets timestamps for supported intervals", () => { + expect(bucketStartFor("2026-07-02T12:34:56.000Z", "5m")).toBe("2026-07-02T12:30:00.000Z"); + expect(bucketStartFor("2026-07-02T12:34:56.000Z", "1h")).toBe("2026-07-02T12:00:00.000Z"); + expect(bucketStartFor("2026-07-02T12:34:56.000Z", "1d")).toBe("2026-07-02T00:00:00.000Z"); + }); + it("derives a deterministic decimals-aware price regardless of swap direction", () => { + expect(deriveCanonicalSwapPrice({ pairAddress: "juno1pair", blockTime: "2026-07-02T12:00:00Z", offerAsset: "ujuno", offerAmount: "1000000", askAsset: "uusdc", returnAmount: "1250000" }, { ujuno: 6, uusdc: 6 })).toMatchObject({ + baseAsset: "ujuno", + quoteAsset: "uusdc", + price: "1.25", + volume: "1", + volumeQuote: "1.25", + }); + expect(deriveCanonicalSwapPrice({ pairAddress: "juno1pair", blockTime: "2026-07-02T12:01:00Z", offerAsset: "uusdc", offerAmount: "2500000", askAsset: "ujuno", returnAmount: "2000000" }, { ujuno: 6, uusdc: 6 })).toMatchObject({ + baseAsset: "ujuno", + quoteAsset: "uusdc", + price: "1.25", + volume: "2", + volumeQuote: "2.5", + }); + }); + it("aggregates swaps into OHLC candles", () => { + const candles = aggregateSwapsToCandles([ + { pairAddress: "juno1pair", blockTime: "2026-07-02T12:01:00Z", offerAsset: "ujuno", offerAmount: "1000000", askAsset: "uusdc", returnAmount: "1000000" }, + { pairAddress: "juno1pair", blockTime: "2026-07-02T12:10:00Z", offerAsset: "ujuno", offerAmount: "1000000", askAsset: "uusdc", returnAmount: "1200000" }, + { pairAddress: "juno1pair", blockTime: "2026-07-02T12:20:00Z", offerAsset: "uusdc", offerAmount: "900000", askAsset: "ujuno", returnAmount: "1000000" }, + ], "1h", { ujuno: 6, uusdc: 6 }); + expect(candles).toHaveLength(1); + expect(candles[0]).toMatchObject({ + bucketStart: "2026-07-02T12:00:00.000Z", + open: "1", + high: "1.19999999999999996", + low: "0.900000000000000022", + close: "0.900000000000000022", + tradeCount: 3, + volume: "3", + }); + }); +}); diff --git a/indexer/dist/test/config.test.js b/indexer/dist/test/config.test.js new file mode 100644 index 000000000..39dbccabc --- /dev/null +++ b/indexer/dist/test/config.test.js @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_START_HEIGHT, loadConfig } from "../src/config.js"; +const performanceEnvNames = [ + "INDEXER_MODE", + "RANGE_SIZE", + "FETCH_WINDOW_SIZE", + "FETCH_CONCURRENCY", + "REALTIME_FETCH_CONCURRENCY", + "RPC_TIMEOUT_MS", + "RPC_MAX_RETRIES", + "INGEST_CANDLES_INLINE", + "INGEST_RESERVE_SNAPSHOTS_INLINE", + "INGEST_AGGREGATES_INLINE", + "INGEST_BULK_STAGING_ENABLED", + "READ_MODEL_REFRESH_INTERVAL_MS", +]; +function withEnv(overrides, run) { + const previous = { ...process.env }; + try { + for (const name of ["DATABASE_URL", "START_HEIGHT", ...performanceEnvNames]) + delete process.env[name]; + for (const [name, value] of Object.entries(overrides)) { + if (value === undefined) + delete process.env[name]; + else + process.env[name] = value; + } + run(); + } + finally { + process.env = previous; + } +} +describe("config", () => { + it("loads sane defaults", () => { + withEnv({}, () => { + const config = loadConfig(); + expect(config.chainId).toBe("juno-1"); + expect(config.databaseUrl).toBe("postgres://postgres:postgres@localhost:5432/astroport_indexer"); + expect(config.startHeight).toBe(DEFAULT_START_HEIGHT); + expect(config.batchSize).toBeGreaterThan(0); + expect(config.wsUrl).toContain("websocket"); + expect(config.indexerMode).toBe("realtime"); + expect(config.rangeSize).toBe(5_000); + expect(config.fetchWindowSize).toBe(250); + expect(config.fetchConcurrency).toBe(32); + expect(config.realtimeFetchConcurrency).toBe(8); + expect(config.rpcTimeoutMs).toBe(10_000); + expect(config.rpcMaxRetries).toBe(5); + expect(config.ingestCandlesInline).toBe(true); + expect(config.ingestReserveSnapshotsInline).toBe(true); + expect(config.ingestAggregatesInline).toBe(false); + expect(config.ingestBulkStagingEnabled).toBe(false); + expect(config.readModelRefreshIntervalMs).toBe(15_000); + expect(config.priceProviderName).toBe("provider"); + expect(config.priceCacheTtlMs).toBe(300_000); + expect(config.priceAllowStale).toBe(true); + expect(config.apiPort).toBe(8787); + }); + }); + it("loads performance runtime overrides", () => { + withEnv({ + INDEXER_MODE: "catchup", + RANGE_SIZE: "10000", + FETCH_WINDOW_SIZE: "500", + FETCH_CONCURRENCY: "64", + REALTIME_FETCH_CONCURRENCY: "4", + RPC_TIMEOUT_MS: "20000", + RPC_MAX_RETRIES: "7", + INGEST_CANDLES_INLINE: "false", + INGEST_RESERVE_SNAPSHOTS_INLINE: "0", + INGEST_AGGREGATES_INLINE: "true", + INGEST_BULK_STAGING_ENABLED: "yes", + READ_MODEL_REFRESH_INTERVAL_MS: "0", + }, () => { + expect(loadConfig()).toMatchObject({ + indexerMode: "catchup", + rangeSize: 10_000, + fetchWindowSize: 500, + fetchConcurrency: 64, + realtimeFetchConcurrency: 4, + rpcTimeoutMs: 20_000, + rpcMaxRetries: 7, + ingestCandlesInline: false, + ingestReserveSnapshotsInline: false, + ingestAggregatesInline: true, + ingestBulkStagingEnabled: true, + readModelRefreshIntervalMs: 0, + }); + }); + }); + it("validates integer values", () => { + withEnv({ START_HEIGHT: "not-a-number" }, () => { + expect(() => loadConfig()).toThrow(/START_HEIGHT/); + }); + }); + it("validates indexer mode", () => { + withEnv({ INDEXER_MODE: "fast" }, () => { + expect(() => loadConfig()).toThrow(/INDEXER_MODE must be either "realtime" or "catchup"/); + }); + }); + it("requires concurrency and window sizes to be at least one", () => { + withEnv({ FETCH_WINDOW_SIZE: "0" }, () => { + expect(() => loadConfig()).toThrow(/FETCH_WINDOW_SIZE must be an integer greater than or equal to 1/); + }); + withEnv({ FETCH_CONCURRENCY: "0" }, () => { + expect(() => loadConfig()).toThrow(/FETCH_CONCURRENCY must be an integer greater than or equal to 1/); + }); + withEnv({ REALTIME_FETCH_CONCURRENCY: "0" }, () => { + expect(() => loadConfig()).toThrow(/REALTIME_FETCH_CONCURRENCY must be an integer greater than or equal to 1/); + }); + }); + it("requires fetch concurrency to fit within the fetch window", () => { + withEnv({ FETCH_WINDOW_SIZE: "10", FETCH_CONCURRENCY: "11" }, () => { + expect(() => loadConfig()).toThrow(/FETCH_CONCURRENCY must be less than or equal to FETCH_WINDOW_SIZE/); + }); + }); + it("allows non-negative retry and timeout values", () => { + withEnv({ RPC_TIMEOUT_MS: "0", RPC_MAX_RETRIES: "0" }, () => { + expect(loadConfig()).toMatchObject({ rpcTimeoutMs: 0, rpcMaxRetries: 0 }); + }); + }); +}); diff --git a/indexer/dist/test/db.test.js b/indexer/dist/test/db.test.js new file mode 100644 index 000000000..41e731507 --- /dev/null +++ b/indexer/dist/test/db.test.js @@ -0,0 +1,533 @@ +import { mkdtemp, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { describe, expect, it } from "vitest"; +import { backfillTokenCandles, claimSnapshotJobs, enqueueSnapshotJobs, listMigrationFiles, markSnapshotJobFailed, markSnapshotJobSucceeded, processNextCandleJob, recordProcessedBlock, refreshApiReadModels, runMigrations, stageAndMergeBatch, upsertPoolStateSnapshot, writeNormalizedEvent, writeNormalizedEvents } from "../src/db.js"; +class FakeMigrationPool { + queries = []; + applied = new Set(); + async query(text, values) { + this.queries.push({ text, values }); + if (text === "SELECT version FROM schema_migrations") { + return { rows: [...this.applied].map((version) => ({ version })) }; + } + if (text.startsWith("INSERT INTO schema_migrations")) { + this.applied.add(String(values?.[0])); + return { rows: [] }; + } + return { rows: [] }; + } +} +class FakeBlockClient { + rowsByKey = new Map(); + queries = []; + nextWriteRowCount = 1; + async query(text, values) { + this.queries.push({ text, values }); + if (text.includes("FROM processed_blocks") && text.includes("height = $2 - 1")) { + const rows = (this.rowsByKey.get(`previous:${String(values?.[1])}`) ?? []); + return { rows, rowCount: rows.length }; + } + if (text.includes("FROM processed_blocks") && text.includes("height = $2")) { + const rows = (this.rowsByKey.get(`existing:${String(values?.[1])}`) ?? []); + return { rows, rowCount: rows.length }; + } + if (text.includes("INSERT INTO processed_blocks")) + return { rows: [], rowCount: this.nextWriteRowCount }; + if (text.includes("INSERT INTO pool_state_snapshots")) + return { rows: [], rowCount: 1 }; + if (text.includes("INSERT INTO snapshot_jobs")) + return { rows: [], rowCount: 1 }; + if (text.includes("WITH claimable")) + return { rows: [{ id: "7", chain_id: "juno-1", pair_address: "juno1pair", height: "39381355", block_time: "2026-07-01T03:01:00Z", reason: "touched", status: "leased", attempts: 1 }], rowCount: 1 }; + if (text.includes("UPDATE snapshot_jobs")) + return { rows: [], rowCount: 1 }; + if (text.includes("FROM pools") && text.includes("pair_address")) { + const rows = (this.rowsByKey.get(`pool:${String(values?.[1])}`) ?? []); + return { rows, rowCount: rows.length }; + } + return { rows: [], rowCount: 1 }; + } +} +class FakeCandleClient { + metadataRows; + swapRow; + poolRows; + queries = []; + constructor(metadataRows = [{ asset: "ujuno", decimals: 6 }, { asset: "factory/token18", decimals: 18 }], swapRow = { pair_address: "juno1pair", block_time: "2026-07-01T03:01:00Z", offer_asset: "factory/backfill18", offer_amount: "2000000000000000000", ask_asset: "ujuno-backfill", return_amount: "3000000", height: "39381355", tx_hash: "tx", msg_index: "0", event_index: "0" }, poolRows = [{ id: "pool-1", pair_address: "juno1pair" }]) { + this.metadataRows = metadataRows; + this.swapRow = swapRow; + this.poolRows = poolRows; + } + async query(text, values) { + this.queries.push({ text, values }); + if (text.includes("INSERT INTO pools")) { + this.poolRows = [{ id: "pool-created", pair_address: String(values?.[1]) }, ...this.poolRows]; + return { rows: [], rowCount: 1 }; + } + if (text.includes("INSERT INTO swaps")) + return { rows: [{ id: "swap-1", pool_id: values?.[1] ?? null }], rowCount: 1 }; + if (text.includes("INSERT INTO candle_jobs")) + return { rows: [], rowCount: 1 }; + if (text.includes("WITH next_job")) + return { rows: [{ id: "job-1", chain_id: "juno-1", pair_address: "juno1pair", from_time: "2026-07-01T00:00:00.000Z", to_time: "2026-07-02T00:00:00.000Z", attempts: 1, worker_id: values?.[1] }], rowCount: 1 }; + if (text.includes("UPDATE candle_jobs")) + return { rows: [], rowCount: 1 }; + if (text.includes("FROM swaps")) + return { rows: [this.swapRow], rowCount: 1 }; + if (text.includes("FROM asset_metadata")) { + const requested = new Set(values?.[1] ?? []); + const rows = this.metadataRows.filter((row) => requested.has(row.asset)); + return { rows: rows, rowCount: rows.length }; + } + if (text.includes("FROM pools") && text.includes("pair_address")) { + const pairAddress = String(values?.[1]); + const rows = this.poolRows.filter((row) => row.pair_address === pairAddress || row.pair_address === undefined); + return { rows: rows, rowCount: rows.length }; + } + if (text.includes("INSERT INTO token_candles")) + return { rows: [], rowCount: 1 }; + return { rows: [], rowCount: 0 }; + } +} +class FakeStageClient { + queries = []; + processedBlockRowCount = 1; + previousBlockHash; + async query(text, values) { + this.queries.push({ text, values }); + if (text.includes("FROM processed_blocks") && text.includes("height = $2")) { + const rows = this.previousBlockHash ? [{ block_hash: this.previousBlockHash }] : []; + return { rows, rowCount: rows.length }; + } + if (text.includes("INSERT INTO processed_blocks")) + return { rows: [], rowCount: this.processedBlockRowCount }; + return { rows: [], rowCount: 1 }; + } +} +describe("migration runner", () => { + it("lists repository migrations from the default runtime path", async () => { + await expect(listMigrationFiles()).resolves.toEqual([ + "001_init.sql", + "002_pool_candles.sql", + "003_api_pricing_readiness.sql", + "004_pool_state_source_precedence.sql", + "005_snapshot_jobs.sql", + "006_candle_jobs.sql", + "007_bulk_staging.sql", + "008_read_models.sql", + "009_juno_stats_derivation.sql", + ]); + }); + it("lists only SQL migrations in deterministic order", async () => { + const dir = await mkdtemp(join(tmpdir(), "juno-indexer-migration-list-")); + await writeFile(join(dir, "002_next.sql"), "SELECT 2;"); + await writeFile(join(dir, "README.md"), "not a migration"); + await writeFile(join(dir, "001_init.sql"), "SELECT 1;"); + await expect(listMigrationFiles(dir)).resolves.toEqual(["001_init.sql", "002_next.sql"]); + }); + it("records migrations once and skips already-applied files on subsequent runs", async () => { + const dir = await mkdtemp(join(tmpdir(), "juno-indexer-migrations-")); + await writeFile(join(dir, "001_init.sql"), "SELECT 1;"); + await writeFile(join(dir, "002_next.sql"), "SELECT 2;"); + const pool = new FakeMigrationPool(); + await expect(runMigrations(pool, dir)).resolves.toEqual(["001_init.sql", "002_next.sql"]); + const firstRunSqlExecutions = pool.queries.filter((query) => query.text === "SELECT 1;" || query.text === "SELECT 2;"); + expect(firstRunSqlExecutions).toHaveLength(2); + pool.queries = []; + await expect(runMigrations(pool, dir)).resolves.toEqual([]); + const secondRunSqlExecutions = pool.queries.filter((query) => query.text === "SELECT 1;" || query.text === "SELECT 2;"); + expect(secondRunSqlExecutions).toHaveLength(0); + }); +}); +describe("processed block recording", () => { + it("rejects a conflicting block hash for an already processed height", async () => { + const client = new FakeBlockClient(); + client.rowsByKey.set("existing:39381305", [{ block_hash: "old-hash", parent_hash: "parent" }]); + await expect(recordProcessedBlock(client, { + chainId: "juno-1", + height: 39381305, + blockHash: "new-hash", + parentHash: "parent", + blockTime: "2026-07-01T03:00:00Z", + txCount: 1, + })).rejects.toThrow(/block hash mismatch/i); + }); + it("rejects a parent-hash mismatch against the previous processed block", async () => { + const client = new FakeBlockClient(); + client.rowsByKey.set("previous:39381306", [{ block_hash: "expected-parent" }]); + await expect(recordProcessedBlock(client, { + chainId: "juno-1", + height: 39381306, + blockHash: "child-hash", + parentHash: "different-parent", + blockTime: "2026-07-01T03:00:06Z", + txCount: 1, + })).rejects.toThrow(/parent hash mismatch/i); + }); + it("rejects an atomic write conflict when the guarded upsert affects no rows", async () => { + const client = new FakeBlockClient(); + client.nextWriteRowCount = 0; + await expect(recordProcessedBlock(client, { + chainId: "juno-1", + height: 39381307, + blockHash: "late-conflict-hash", + parentHash: "parent", + blockTime: "2026-07-01T03:00:12Z", + txCount: 1, + })).rejects.toThrow(/processed block conflict/i); + const insert = client.queries.find((query) => query.text.includes("INSERT INTO processed_blocks")); + expect(insert?.text).toContain("WHERE processed_blocks.chain_id = EXCLUDED.chain_id"); + expect(insert?.text).toContain("processed_blocks.block_hash = EXCLUDED.block_hash"); + }); +}); +describe("bulk staging merge writer", () => { + it("stages decoded rows, merges canonical tables in dependency order, and advances the cursor after merge SQL", async () => { + const client = new FakeStageClient(); + await stageAndMergeBatch(client, { + batchId: "00000000-0000-4000-8000-000000000001", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + writeCandlesInline: false, + enqueueSnapshots: true, + blocks: [{ + chainId: "juno-1", + height: 39381355, + blockHash: "block-39381355", + parentHash: "block-39381354", + blockTime: "2026-07-01T03:01:00Z", + txCount: 1, + events: [ + { kind: "pool_created", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 0, factoryAddress: "juno1factory", pairAddress: "juno1pair", assetInfos: ["ujuno", "uusdc"], raw: {} }, + { kind: "swap", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 1, pairAddress: "juno1pair", trader: "juno1trader", offerAsset: "ujuno", offerAmount: "1", askAsset: "uusdc", returnAmount: "2", raw: {} }, + { kind: "provide", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 2, pairAddress: "juno1pair", provider: "juno1provider", assets: [{ asset: "ujuno", amount: "1" }], shareAmount: "1", raw: {} }, + { kind: "incentive", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 3, incentivesAddress: "juno1incentives", action: "bond", userAddress: "juno1user", amount: "1", raw: {} }, + ], + }], + }); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_processed_blocks"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_pools"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_swaps"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_liquidity_events"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_incentive_events"))).toBe(true); + const processedMergeIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO processed_blocks")); + const poolMergeIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO pools") && query.text.includes("FROM stage_pools")); + const swapMergeIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO swaps") && query.text.includes("FROM stage_swaps")); + const cursorIndex = client.queries.findIndex((query) => query.text.includes("UPDATE indexer_cursors")); + expect(processedMergeIndex).toBeGreaterThanOrEqual(0); + expect(poolMergeIndex).toBeGreaterThan(processedMergeIndex); + expect(swapMergeIndex).toBeGreaterThan(poolMergeIndex); + expect(cursorIndex).toBeGreaterThan(swapMergeIndex); + expect(client.queries[cursorIndex]?.values).toEqual(["astroport-juno-v1", 39381355, "block-39381355"]); + expect(client.queries.some((query) => query.text.includes("INSERT INTO candle_jobs"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO snapshot_jobs"))).toBe(true); + }); + it("does not advance the cursor when a staging merge detects a processed block conflict", async () => { + const client = new FakeStageClient(); + client.processedBlockRowCount = 0; + await expect(stageAndMergeBatch(client, { + batchId: "00000000-0000-4000-8000-000000000002", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + blocks: [{ chainId: "juno-1", height: 12, blockHash: "new", parentHash: "old", blockTime: "2026-07-01T03:01:00Z", txCount: 0, events: [] }], + })).rejects.toThrow(/processed block conflict/); + expect(client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + }); + it("rejects non-contiguous or forked staged block ranges before advancing the cursor", async () => { + const client = new FakeStageClient(); + await expect(stageAndMergeBatch(client, { + batchId: "00000000-0000-4000-8000-000000000003", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + blocks: [ + { chainId: "juno-1", height: 12, blockHash: "block-12", parentHash: "block-11", blockTime: "2026-07-01T03:01:00Z", txCount: 0, events: [] }, + { chainId: "juno-1", height: 13, blockHash: "block-13", parentHash: "different-parent", blockTime: "2026-07-01T03:01:06Z", txCount: 0, events: [] }, + ], + })).rejects.toThrow(/parent hash mismatch/); + expect(client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + const previousClient = new FakeStageClient(); + previousClient.previousBlockHash = "canonical-11"; + await expect(stageAndMergeBatch(previousClient, { + batchId: "00000000-0000-4000-8000-000000000004", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + blocks: [{ chainId: "juno-1", height: 12, blockHash: "block-12", parentHash: "fork-11", blockTime: "2026-07-01T03:01:00Z", txCount: 0, events: [] }], + })).rejects.toThrow(/parent hash mismatch/); + expect(previousClient.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + }); +}); +describe("swap candle writes", () => { + it("uses asset_metadata decimals for indexer candle price and volume math", async () => { + const client = new FakeCandleClient(); + await writeNormalizedEvent(client, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1pair", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }); + const swapInsert = client.queries.find((query) => query.text.includes("INSERT INTO swaps")); + expect(swapInsert?.values?.slice(0, 4)).toEqual(["juno-1", "pool-1", "juno1pair", 39381355]); + const metadata = client.queries.find((query) => query.text.includes("FROM asset_metadata")); + expect(metadata?.values).toEqual(["juno-1", ["factory/token18", "ujuno"]]); + const candleInsert = client.queries.find((query) => query.text.includes("INSERT INTO token_candles")); + expect(candleInsert?.values?.slice(3, 10)).toEqual(["factory/token18", "ujuno", "5m", "2026-07-01T03:00:00.000Z", "1.5", "2", "3"]); + }); + it("skips candle writes when inline candle option is disabled", async () => { + const client = new FakeCandleClient(); + await writeNormalizedEvent(client, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1pair", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }, { writeCandlesInline: false }); + expect(client.queries.some((query) => query.text.includes("INSERT INTO swaps"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO candle_jobs"))).toBe(true); + const jobInsert = client.queries.find((query) => query.text.includes("INSERT INTO candle_jobs")); + expect(jobInsert?.values).toEqual(["juno-1", "juno1pair", "2026-07-01T00:00:00.000Z", "2026-07-02T00:00:00.000Z"]); + expect(client.queries.some((query) => query.text.includes("FROM asset_metadata"))).toBe(false); + expect(client.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + it("skips candle writes when either swap asset lacks valid decimals", async () => { + const client = new FakeCandleClient([{ asset: "factory/missing18", decimals: 18 }, { asset: "ujuno-missing", decimals: null }]); + await writeNormalizedEvent(client, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1pair", + offerAsset: "factory/missing18", + offerAmount: "2000000000000000000", + askAsset: "ujuno-missing", + returnAmount: "3000000", + raw: {}, + }); + expect(client.queries.some((query) => query.text.includes("INSERT INTO swaps"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + it("uses decimals for candle backfills and skips degraded metadata", async () => { + const okClient = new FakeCandleClient([{ asset: "factory/backfill18", decimals: 18 }, { asset: "ujuno-backfill", decimals: 6 }]); + await expect(backfillTokenCandles(okClient, { chainId: "juno-1" })).resolves.toBe(1); + const backfillInsert = okClient.queries.find((query) => query.text.includes("INSERT INTO token_candles")); + expect(backfillInsert?.values?.slice(3, 15)).toEqual(["factory/backfill18", "ujuno-backfill", "5m", "2026-07-01T03:00:00.000Z", "1.5", "1.5", "1.5", "1.5", "2", "3", 1, "backfill"]); + const badClient = new FakeCandleClient([{ asset: "factory/backfill-bad18", decimals: 309 }, { asset: "ujuno-backfill-bad", decimals: 6 }], { pair_address: "juno1pair", block_time: "2026-07-01T03:01:00Z", offer_asset: "factory/backfill-bad18", offer_amount: "2000000000000000000", ask_asset: "ujuno-backfill-bad", return_amount: "3000000", height: "39381355", tx_hash: "tx", msg_index: "0", event_index: "0" }); + await expect(backfillTokenCandles(badClient, { chainId: "juno-1" })).resolves.toBe(1); + expect(badClient.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + it("worker claims a candle job, rebuilds candles through shared helper, and marks completion", async () => { + const client = new FakeCandleClient([{ asset: "factory/backfill18", decimals: 18 }, { asset: "ujuno-backfill", decimals: 6 }]); + await expect(processNextCandleJob(client, { chainId: "juno-1", workerId: "worker-1" })).resolves.toMatchObject({ + id: "job-1", + pairAddress: "juno1pair", + }); + const claim = client.queries.find((query) => query.text.includes("FOR UPDATE SKIP LOCKED")); + expect(claim?.values?.slice(0, 2)).toEqual(["juno-1", "worker-1"]); + const swapRead = client.queries.find((query) => query.text.includes("FROM swaps")); + expect(swapRead?.text).toContain("ORDER BY height ASC, msg_index ASC, event_index ASC, id ASC"); + expect(swapRead?.values).toEqual(["juno-1", "juno1pair", "2026-07-01T00:00:00.000Z", "2026-07-02T00:00:00.000Z", 2147483647, true]); + const candleInsert = client.queries.find((query) => query.text.includes("INSERT INTO token_candles")); + expect(candleInsert?.values?.slice(3, 15)).toEqual(["factory/backfill18", "ujuno-backfill", "5m", "2026-07-01T03:00:00.000Z", "1.5", "1.5", "1.5", "1.5", "2", "3", 1, "worker"]); + const complete = client.queries.find((query) => query.text.includes("rerun_requested")); + expect(complete?.text).toContain("AND status = 'running'"); + expect(complete?.text).toContain("AND worker_id = $2"); + expect(complete?.text).toContain("AND attempts = $3"); + expect(complete?.values).toEqual(["job-1", "worker-1", 1, 1]); + }); + it("writes pool discovery before same-batch pair events regardless of emitted order", async () => { + const client = new FakeCandleClient(undefined, undefined, []); + await writeNormalizedEvents(client, "juno-1", [ + { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1newpair", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }, + { + kind: "pool_created", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 1, + factoryAddress: "juno1factory", + pairAddress: "juno1newpair", + assetInfos: ["factory/token18", "ujuno"], + raw: {}, + }, + ]); + const poolInsertIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO pools")); + const swapInsertIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO swaps")); + expect(poolInsertIndex).toBeGreaterThanOrEqual(0); + expect(swapInsertIndex).toBeGreaterThan(poolInsertIndex); + const swapInsert = client.queries[swapInsertIndex]; + expect(swapInsert?.values?.slice(0, 4)).toEqual(["juno-1", "pool-created", "juno1newpair", 39381355]); + }); + it("skips swap persistence for unknown pair contracts", async () => { + const client = new FakeCandleClient(undefined, undefined, []); + await writeNormalizedEvent(client, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1unrelated", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }); + expect(client.queries.some((query) => query.text.includes("FROM pools"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO swaps"))).toBe(false); + expect(client.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + it("writes known liquidity events with pool_id", async () => { + const client = new FakeCandleClient(); + await writeNormalizedEvent(client, "juno-1", { + kind: "provide", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx-liq-known", + msgIndex: 0, + eventIndex: 1, + pairAddress: "juno1pair", + provider: "juno1provider", + assets: [{ asset: "ujuno", amount: "1" }], + shareAmount: "1", + raw: {}, + }); + const insert = client.queries.find((query) => query.text.includes("INSERT INTO liquidity_events")); + expect(insert?.values?.slice(0, 4)).toEqual(["juno-1", "pool-1", "juno1pair", 39381355]); + }); + it("skips liquidity persistence for unknown pair contracts", async () => { + const client = new FakeCandleClient(undefined, undefined, []); + await writeNormalizedEvent(client, "juno-1", { + kind: "provide", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx-liq", + msgIndex: 0, + eventIndex: 1, + pairAddress: "juno1unrelated", + provider: "juno1provider", + assets: [{ asset: "ujuno", amount: "1" }], + shareAmount: "1", + raw: {}, + }); + expect(client.queries.some((query) => query.text.includes("FROM pools"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO liquidity_events"))).toBe(false); + }); +}); +describe("pool state snapshots", () => { + it("enqueues reserve snapshot jobs idempotently for known pools only", async () => { + const client = new FakeBlockClient(); + await expect(enqueueSnapshotJobs(client, { + chainId: "juno-1", + pairAddresses: ["juno1pair", "juno1pair", "juno1missing"], + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + reason: "touched", + })).resolves.toBe(1); + const insert = client.queries.find((query) => query.text.includes("INSERT INTO snapshot_jobs")); + expect(insert?.text).toContain("FROM pools p"); + expect(insert?.text).toContain("ON CONFLICT (chain_id, pair_address, height, reason) DO NOTHING"); + expect(insert?.values).toEqual(["juno-1", ["juno1pair", "juno1missing"], 39381355, "2026-07-01T03:01:00Z", "touched"]); + }); + it("claims snapshot jobs with skip-locked leases and updates terminal state", async () => { + const client = new FakeBlockClient(); + await expect(claimSnapshotJobs(client, { chainId: "juno-1", limit: 10, leaseSeconds: 30, maxAttempts: 5 })).resolves.toEqual([ + { id: "7", chainId: "juno-1", pairAddress: "juno1pair", height: 39381355, blockTime: "2026-07-01T03:01:00Z", reason: "touched", status: "leased", attempts: 1 }, + ]); + await markSnapshotJobSucceeded(client, { jobId: "7", attempt: 1 }); + await markSnapshotJobFailed(client, { jobId: "8", attempt: 2, error: "temporary", permanent: false, maxAttempts: 5 }); + const claim = client.queries.find((query) => query.text.includes("WITH claimable")); + expect(claim?.text).toContain("FOR UPDATE SKIP LOCKED"); + expect(claim?.values).toEqual(["juno-1", 10, "30 seconds", 5]); + const success = client.queries.find((query) => query.text.includes("status = 'succeeded'")); + expect(success?.text).toContain("AND status = 'leased'"); + expect(success?.text).toContain("AND attempts = $2"); + expect(success?.values).toEqual(["7", 1]); + const failure = client.queries.find((query) => query.text.includes("last_error = $4")); + expect(failure?.text).toContain("AND status = 'leased'"); + expect(failure?.text).toContain("AND attempts = $2"); + expect(failure?.values).toEqual(["8", 2, false, "temporary", 5]); + }); + it("upserts reserve snapshots idempotently by pool, height, and source", async () => { + const client = new FakeBlockClient(); + client.rowsByKey.set("pool:juno1pair", [{ id: "pool-1" }]); + await upsertPoolStateSnapshot(client, { + chainId: "juno-1", + pairAddress: "juno1pair", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + reserves: [{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }], + totalShare: "789", + source: "event", + }); + const select = client.queries.find((query) => query.text.includes("FROM pools")); + expect(select?.values).toEqual(["juno-1", "juno1pair"]); + const insert = client.queries.find((query) => query.text.includes("INSERT INTO pool_state_snapshots")); + expect(insert?.text).toContain("ON CONFLICT (pool_id, height, source) DO UPDATE"); + expect(insert?.values).toEqual(["pool-1", 39381355, "2026-07-01T03:01:00Z", JSON.stringify([{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }]), "789", "event"]); + }); + it("rejects snapshots for unknown pools instead of writing orphan state", async () => { + const client = new FakeBlockClient(); + await expect(upsertPoolStateSnapshot(client, { + chainId: "juno-1", + pairAddress: "juno1missing", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + reserves: [], + })).rejects.toThrow(/unknown pair juno1missing/i); + }); +}); +describe("API read model refresh", () => { + it("calls the SQL refresh helper and maps affected rows", async () => { + const client = { + queries: [], + async query(text, values) { + this.queries.push({ text, values }); + return { rows: [{ model: "latest_pool_state", rows_affected: "1" }, { model: "protocol_stats_latest", rows_affected: 1 }], rowCount: 2 }; + }, + }; + await expect(refreshApiReadModels(client, { chainId: "juno-1" })).resolves.toEqual([ + { model: "latest_pool_state", rowsAffected: 1 }, + { model: "protocol_stats_latest", rowsAffected: 1 }, + ]); + expect(client.queries[0]).toEqual({ text: "SELECT model, rows_affected FROM refresh_api_read_models($1::text)", values: ["juno-1"] }); + }); +}); diff --git a/indexer/dist/test/events.test.js b/indexer/dist/test/events.test.js new file mode 100644 index 000000000..1c7050ac4 --- /dev/null +++ b/indexer/dist/test/events.test.js @@ -0,0 +1,158 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { attributesToRecord, normalizeBlockEvents, normalizeWasmEvent } from "../src/events.js"; +const context = { chainId: "juno-1", height: 123, blockTime: "2026-07-02T00:00:00Z", txHash: "ABC", msgIndex: 0, eventIndex: 0 }; +const contracts = { factoryAddress: "juno1factory", incentivesAddress: "juno1incentives" }; +const factoryAddress = "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca"; +const incentivesAddress = "juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598"; +const pairAddress = "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv"; +const testDenom = "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323"; +const junoV1Contracts = { factoryAddress, incentivesAddress }; +function fixture(name) { + return JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", "juno-v1", `${name}.json`), "utf8")); +} +function normalizedFixture(name) { + const tx = fixture(name); + return normalizeBlockEvents(tx.events, { chainId: "juno-1", height: tx.height, blockTime: tx.timestamp, txHash: tx.txhash }, junoV1Contracts); +} +describe("event normalization", () => { + it("preserves repeated attributes", () => { + expect(attributesToRecord([ + { key: "asset_info", value: "ujuno" }, + { key: "asset_info", value: "factory/token" }, + ])).toEqual({ asset_info: ["ujuno", "factory/token"] }); + }); + it("normalizes factory pair creation events", () => { + const event = normalizeWasmEvent({ + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1factory" }, + { key: "action", value: "create_pair" }, + { key: "pair_contract_addr", value: "juno1pair" }, + { key: "liquidity_token_addr", value: "factory/juno1pair/astroport/share" }, + { key: "pair_type", value: "xyk" }, + { key: "asset_info", value: "ujuno" }, + { key: "asset_info", value: "factory/juno/token" }, + ], + }, context, contracts); + expect(event).toMatchObject({ + kind: "pool_created", + pairAddress: "juno1pair", + liquidityTokenAddress: "factory/juno1pair/astroport/share", + poolType: "xyk", + assetInfos: ["ujuno", "factory/juno/token"], + }); + }); + it("normalizes swap events emitted by pair contracts", () => { + const event = normalizeWasmEvent({ + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000" }, + { key: "ask_asset", value: "factory/juno/token" }, + { key: "return_amount", value: "990" }, + { key: "commission_amount", value: "3" }, + ], + }, context, contracts); + expect(event).toMatchObject({ + kind: "swap", + pairAddress: "juno1pair", + trader: "juno1trader", + offerAsset: "ujuno", + offerAmount: "1000", + returnAmount: "990", + commissionAmount: "3", + }); + }); + it("normalizes provide and withdraw liquidity events", () => { + const events = normalizeBlockEvents([ + { + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "provide_liquidity" }, + { key: "sender", value: "juno1lp" }, + { key: "asset", value: "ujuno" }, + { key: "asset", value: "factory/juno/token" }, + { key: "amount", value: "1000" }, + { key: "amount", value: "2000" }, + { key: "share", value: "1414" }, + ], + }, + { + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "withdraw_liquidity" }, + { key: "sender", value: "juno1lp" }, + { key: "share", value: "100" }, + ], + }, + ], { chainId: "juno-1", height: 124, blockTime: "2026-07-02T00:01:00Z", txHash: "DEF" }, contracts); + expect(events.map((event) => event.kind)).toEqual(["provide", "withdraw"]); + expect(events[0]).toMatchObject({ provider: "juno1lp", assets: [{ asset: "ujuno", amount: "1000" }, { asset: "factory/juno/token", amount: "2000" }] }); + }); + it("normalizes incentives events from the configured incentives contract", () => { + const event = normalizeWasmEvent({ + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1incentives" }, + { key: "action", value: "deposit" }, + { key: "sender", value: "juno1staker" }, + { key: "lp_token", value: "factory/juno1pair/astroport/share" }, + { key: "amount", value: "500" }, + ], + }, context, contracts); + expect(event).toMatchObject({ + kind: "incentive", + action: "deposit", + userAddress: "juno1staker", + lpTokenAddress: "factory/juno1pair/astroport/share", + amount: "500", + }); + }); + it("normalizes the real Juno v1 create-pair deployment tx fixture", () => { + const events = normalizedFixture("create-pair"); + const created = events.find((event) => event.kind === "pool_created"); + expect(created).toMatchObject({ + kind: "pool_created", + height: 39381305, + txHash: "8EFD15276286C15D5CFF11B55D49522D2987E16F8220DE671CA0971E586BCD8E", + factoryAddress, + pairAddress, + }); + }); + it("normalizes real Juno v1 liquidity tx fixtures with asset amounts", () => { + const seed = normalizedFixture("seed-liquidity"); + expect(seed).toHaveLength(1); + expect(seed[0]).toMatchObject({ + kind: "provide", + pairAddress, + shareAmount: "9999000", + assets: [{ amount: "10000000", asset: "ujuno" }, { amount: "10000000", asset: testDenom }], + }); + const add = normalizedFixture("smoke-add-liquidity"); + expect(add[0]).toMatchObject({ kind: "provide", shareAmount: "9900", assets: [{ amount: "10000", asset: "ujuno" }, { amount: "9803", asset: testDenom }] }); + const withdraw = normalizedFixture("smoke-withdraw-liquidity"); + expect(withdraw[0]).toMatchObject({ kind: "withdraw", shareAmount: "1000", assets: [{ amount: "1010", asset: "ujuno" }, { amount: "990", asset: testDenom }] }); + }); + it("normalizes the real Juno v1 smoke swap tx fixture", () => { + const events = normalizedFixture("smoke-swap"); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + kind: "swap", + pairAddress, + trader: "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + offerAsset: "ujuno", + askAsset: testDenom, + offerAmount: "100000", + returnAmount: "98712", + spreadAmount: "991", + commissionAmount: "297", + }); + }); +}); diff --git a/indexer/dist/test/indexer.test.js b/indexer/dist/test/indexer.test.js new file mode 100644 index 000000000..ff93119f8 --- /dev/null +++ b/indexer/dist/test/indexer.test.js @@ -0,0 +1,291 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_CONTRACTS } from "../src/config.js"; +import { Indexer } from "../src/indexer.js"; +class FakeIndexerClient { + queries = []; + failProcessedBlockHeight; + failStagedMerge = false; + onBegin; + async query(text, values) { + this.queries.push({ text, values }); + if (text === "BEGIN") + this.onBegin?.(); + if (text.includes("RETURNING last_height")) + return { rows: [{ last_height: "10" }], rowCount: 1 }; + if (text.includes("FROM processed_blocks")) + return { rows: [], rowCount: 0 }; + if (text.includes("INSERT INTO processed_blocks") && text.includes("FROM stage_processed_blocks")) { + if (this.failStagedMerge) + throw new Error("staged merge failed"); + return { rows: [], rowCount: 1 }; + } + if (text.includes("INSERT INTO processed_blocks")) { + if (values?.[1] === this.failProcessedBlockHeight) + throw new Error(`boom at ${this.failProcessedBlockHeight}`); + return { rows: [], rowCount: 1 }; + } + if (text.includes("INSERT INTO swaps")) + return { rows: [{ id: "swap-1", pool_id: "pool-1" }], rowCount: 1 }; + if (text.includes("FROM asset_metadata")) + return { rows: [{ asset: "ujuno", decimals: 6 }, { asset: "uusdc", decimals: 6 }], rowCount: 2 }; + if (text.includes("FROM pools") && text.includes("ANY($2::text[])")) { + const requested = new Set(values?.[1] ?? []); + const rows = ["juno1pair"].filter((pair) => requested.has(pair)).map((pair_address) => ({ pair_address })); + return { rows: rows, rowCount: rows.length }; + } + if (text.includes("FROM pools") && text.includes("pair_address")) + return { rows: [{ id: "pool-1", pair_address: "juno1pair" }], rowCount: 1 }; + if (text.includes("INSERT INTO token_candles")) + return { rows: [], rowCount: 1 }; + if (text.includes("INSERT INTO pool_state_snapshots")) + return { rows: [], rowCount: 1 }; + if (text.includes("INSERT INTO snapshot_jobs")) + return { rows: [], rowCount: 1 }; + if (text.includes("UPDATE indexer_cursors")) + return { rows: [], rowCount: 1 }; + return { rows: [], rowCount: 0 }; + } + release() { } +} +class FakeIndexerPool { + client = new FakeIndexerClient(); + async connect() { return this.client; } +} +const baseConfig = { + databaseUrl: "postgres://test", + rpcUrl: "https://rpc.example", + restUrl: "https://lcd.example", + wsUrl: "wss://rpc.example/websocket", + chainId: "juno-1", + factoryAddress: DEFAULT_CONTRACTS.factory, + routerAddress: DEFAULT_CONTRACTS.router, + incentivesAddress: DEFAULT_CONTRACTS.incentives, + oracleAddress: DEFAULT_CONTRACTS.oracle, + nativeCoinRegistryAddress: DEFAULT_CONTRACTS.nativeCoinRegistry, + startHeight: 11, + confirmationDepth: 2, + pollIntervalMs: 1, + batchSize: 1, + dryRun: false, + cursorId: "astroport-juno-v1", + indexerMode: "realtime", + rangeSize: 5_000, + fetchWindowSize: 250, + fetchConcurrency: 32, + realtimeFetchConcurrency: 8, + rpcTimeoutMs: 10_000, + rpcMaxRetries: 5, + ingestCandlesInline: true, + ingestReserveSnapshotsInline: true, + ingestAggregatesInline: false, + ingestBulkStagingEnabled: false, + priceProviderName: "provider", + priceCacheTtlMs: 300_000, + priceStaleAfterMs: 1_800_000, + priceAllowStale: true, + priceDevMocks: false, + readModelRefreshIntervalMs: 0, + apiPort: 8787, +}; +afterEach(() => vi.restoreAllMocks()); +function rpcBlock(height, txEvents = []) { + return { + block: { result: { block_id: { hash: `block-${height}` }, block: { header: { time: `2026-07-01T03:00:${String(height).padStart(2, "0")}Z`, last_block_id: { hash: `block-${height - 1}` } }, data: { txs: txEvents.length > 0 ? ["AA=="] : [] } } } }, + results: { result: { txs_results: txEvents.length > 0 ? [{ hash: `tx-${height}`, events: txEvents }] : [] } }, + }; +} +function mockRpcRange(headHeight, blocks, fetchedBlocks, onBlockFetchStart, onBlockFetchEnd) { + return vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/status") { + return { ok: true, json: async () => ({ result: { sync_info: { latest_block_height: String(headHeight), latest_block_hash: "head" } } }) }; + } + const blockMatch = url.match(/^https:\/\/rpc\.example\/block\?height=(\d+)$/); + if (blockMatch) { + const height = Number(blockMatch[1]); + fetchedBlocks?.push(height); + onBlockFetchStart?.(); + await Promise.resolve(); + onBlockFetchEnd?.(); + return { ok: true, json: async () => blocks.get(height)?.block }; + } + const resultsMatch = url.match(/^https:\/\/rpc\.example\/block_results\?height=(\d+)$/); + if (resultsMatch) { + const height = Number(resultsMatch[1]); + return { ok: true, json: async () => blocks.get(height)?.results }; + } + throw new Error(`unexpected fetch: ${url}`); + }); +} +describe("Indexer fetch/decode/ordered writer pipeline", () => { + it("fetches multiple blocks before ordered writing and advances the cursor height by height", async () => { + const blocks = new Map([11, 12, 13].map((height) => [height, rpcBlock(height)])); + const fetchedBlocks = []; + mockRpcRange(15, blocks, fetchedBlocks); + const pool = new FakeIndexerPool(); + const fetchedBeforeFirstWrite = []; + pool.client.onBegin = () => { + if (fetchedBeforeFirstWrite.length === 0) + fetchedBeforeFirstWrite.push(...fetchedBlocks); + }; + await expect(new Indexer({ ...baseConfig, batchSize: 3, fetchConcurrency: 1, realtimeFetchConcurrency: 3 }, pool).runOnce()).resolves.toMatchObject({ processed: 3, cursorHeight: 13 }); + expect(fetchedBeforeFirstWrite.sort((a, b) => a - b)).toEqual([11, 12, 13]); + const cursorUpdates = pool.client.queries.filter((query) => query.text.includes("UPDATE indexer_cursors")); + expect(cursorUpdates.map((query) => query.values?.[1])).toEqual([11, 12, 13]); + }); + it("uses catchup fetch concurrency when the indexer is in catchup mode", async () => { + const blocks = new Map([11, 12, 13, 14].map((height) => [height, rpcBlock(height)])); + let activeBlockFetches = 0; + let maxActiveBlockFetches = 0; + mockRpcRange(16, blocks, undefined, () => { + activeBlockFetches += 1; + maxActiveBlockFetches = Math.max(maxActiveBlockFetches, activeBlockFetches); + }, () => { + activeBlockFetches -= 1; + }); + const pool = new FakeIndexerPool(); + await expect(new Indexer({ ...baseConfig, indexerMode: "catchup", batchSize: 4, fetchConcurrency: 2, realtimeFetchConcurrency: 4 }, pool).runOnce()).resolves.toMatchObject({ processed: 4, cursorHeight: 14 }); + expect(maxActiveBlockFetches).toBe(2); + }); + it("stops later cursor advancement when an ordered block write fails", async () => { + const blocks = new Map([11, 12, 13].map((height) => [height, rpcBlock(height)])); + mockRpcRange(15, blocks); + const pool = new FakeIndexerPool(); + pool.client.failProcessedBlockHeight = 12; + await expect(new Indexer({ ...baseConfig, batchSize: 3, realtimeFetchConcurrency: 3 }, pool).runOnce()).rejects.toThrow(/boom at 12/); + const cursorUpdates = pool.client.queries.filter((query) => query.text.includes("UPDATE indexer_cursors")); + expect(cursorUpdates.map((query) => query.values?.[1])).toEqual([11]); + }); + it("uses the bulk staging writer only for catchup mode when enabled", async () => { + const blocks = new Map([[11, rpcBlock(11, [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }])]]); + mockRpcRange(13, blocks); + const pool = new FakeIndexerPool(); + await expect(new Indexer({ ...baseConfig, indexerMode: "catchup", ingestBulkStagingEnabled: true, ingestCandlesInline: false, batchSize: 1, ingestReserveSnapshotsInline: false }, pool).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO stage_processed_blocks"))).toBe(true); + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO swaps") && query.text.includes("FROM stage_swaps"))).toBe(true); + const cursorUpdates = pool.client.queries.filter((query) => query.text.includes("UPDATE indexer_cursors")); + expect(cursorUpdates.map((query) => query.values?.[1])).toEqual([11]); + }); + it("leaves the cursor unchanged when the bulk staging merge fails", async () => { + const blocks = new Map([[11, rpcBlock(11)]]); + mockRpcRange(13, blocks); + const pool = new FakeIndexerPool(); + pool.client.failStagedMerge = true; + await expect(new Indexer({ ...baseConfig, indexerMode: "catchup", ingestBulkStagingEnabled: true, ingestCandlesInline: false, batchSize: 1 }, pool).runOnce()).rejects.toThrow(/staged merge failed/); + expect(pool.client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + expect(pool.client.queries.some((query) => query.text === "ROLLBACK")).toBe(true); + }); +}); +describe("Indexer reserve snapshots", () => { + it("queries pair pool state at the processed height and writes one lcd snapshot per touched pair", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const url = String(input); + if (url === "https://rpc.example/status") { + return { ok: true, json: async () => ({ result: { sync_info: { latest_block_height: "13", latest_block_hash: "head" } } }) }; + } + if (url === "https://rpc.example/block?height=11") { + return { ok: true, json: async () => ({ result: { block_id: { hash: "block-11" }, block: { header: { time: "2026-07-01T03:01:00Z", last_block_id: { hash: "block-10" } }, data: { txs: ["AA=="] } } } }) }; + } + if (url === "https://rpc.example/block_results?height=11") { + return { ok: true, json: async () => ({ result: { txs_results: [{ hash: "tx", events: [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }] }] } }) }; + } + if (url.startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/")) { + expect(init?.headers).toMatchObject({ "x-cosmos-block-height": "11" }); + return { ok: true, json: async () => ({ data: { assets: [{ info: { native_token: { denom: "ujuno" } }, amount: "123" }, { info: { native_token: { denom: "uusdc" } }, amount: "456" }], total_share: "789" } }) }; + } + throw new Error(`unexpected fetch: ${url}`); + }); + const pool = new FakeIndexerPool(); + await expect(new Indexer(baseConfig, pool).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + const snapshot = pool.client.queries.find((query) => query.text.includes("INSERT INTO pool_state_snapshots")); + expect(snapshot?.values).toEqual(["pool-1", 11, "2026-07-01T03:01:00Z", JSON.stringify([{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }]), "789", "lcd"]); + const lcdCalls = fetchSpy.mock.calls.filter(([input]) => String(input).startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/")); + expect(lcdCalls).toHaveLength(1); + const rangeLog = JSON.parse(String(logSpy.mock.calls.at(-1)?.[0])); + expect(rangeLog).toMatchObject({ + msg: "indexer_range_processed", + role: "indexer", + rangeFrom: 11, + rangeTo: 11, + cursor: 11, + head: 13, + target: 11, + lag: 0, + blocks: 1, + swaps: 1, + liquidityEvents: 0, + incentiveEvents: 0, + }); + expect(rangeLog.durationMs).toEqual(expect.any(Number)); + expect(rangeLog.dbDurationMs).toEqual(expect.any(Number)); + }); + it("keeps cursor progress when LCD reserve snapshots fail", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(console, "warn").mockImplementation(() => undefined); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/status") { + return { ok: true, json: async () => ({ result: { sync_info: { latest_block_height: "13", latest_block_hash: "head" } } }) }; + } + if (url === "https://rpc.example/block?height=11") { + return { ok: true, json: async () => ({ result: { block_id: { hash: "block-11" }, block: { header: { time: "2026-07-01T03:01:00Z", last_block_id: { hash: "block-10" } }, data: { txs: ["AA=="] } } } }) }; + } + if (url === "https://rpc.example/block_results?height=11") { + return { ok: true, json: async () => ({ result: { txs_results: [{ hash: "tx", events: [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }] }] } }) }; + } + if (url.startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/")) { + return { ok: false, status: 500, statusText: "unavailable", json: async () => ({}) }; + } + throw new Error(`unexpected fetch: ${url}`); + }); + const pool = new FakeIndexerPool(); + await expect(new Indexer(baseConfig, pool).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + expect(pool.client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(true); + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO pool_state_snapshots"))).toBe(false); + expect(fetchSpy.mock.calls.filter(([input]) => String(input).startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/"))).toHaveLength(3); + }); + it("does not call LCD pool state when inline reserve snapshots are disabled", async () => { + const swapEvents = [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }]; + const blocks = new Map([[11, rpcBlock(11, swapEvents)]]); + const fetchSpy = mockRpcRange(13, blocks); + const pool = new FakeIndexerPool(); + await expect(new Indexer({ ...baseConfig, ingestReserveSnapshotsInline: false }, pool).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO pool_state_snapshots"))).toBe(false); + const jobInsert = pool.client.queries.find((query) => query.text.includes("INSERT INTO snapshot_jobs")); + expect(jobInsert?.values).toEqual(["juno-1", ["juno1pair"], 11, "2026-07-01T03:00:11Z", "touched"]); + expect(fetchSpy.mock.calls.some(([input]) => String(input).startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/"))).toBe(false); + }); +}); diff --git a/indexer/dist/test/ranges.test.js b/indexer/dist/test/ranges.test.js new file mode 100644 index 000000000..0a509fb87 --- /dev/null +++ b/indexer/dist/test/ranges.test.js @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { Indexer } from "../src/indexer.js"; +import { parseNonNegativeInteger, nextBlockRange } from "../src/ranges.js"; +const testConfig = { + databaseUrl: "postgres://postgres:***@localhost:5432/astroport_indexer_test", + rpcUrl: "http://127.0.0.1:26657", + restUrl: "http://127.0.0.1:1317", + wsUrl: "ws://127.0.0.1:26657/websocket", + chainId: "juno-1", + factoryAddress: "juno1factory", + routerAddress: "juno1router", + incentivesAddress: "juno1incentives", + oracleAddress: "juno1oracle", + nativeCoinRegistryAddress: "juno1registry", + startHeight: 100, + confirmationDepth: 2, + pollIntervalMs: 1, + batchSize: 20, + dryRun: true, + cursorId: "test", + indexerMode: "realtime", + rangeSize: 5_000, + fetchWindowSize: 250, + fetchConcurrency: 32, + realtimeFetchConcurrency: 8, + rpcTimeoutMs: 10_000, + rpcMaxRetries: 5, + ingestCandlesInline: true, + ingestReserveSnapshotsInline: true, + ingestAggregatesInline: false, + ingestBulkStagingEnabled: false, + priceProviderName: "provider", + priceCacheTtlMs: 300_000, + priceStaleAfterMs: 1_800_000, + priceAllowStale: true, + priceDevMocks: false, + readModelRefreshIntervalMs: 0, + apiPort: 8787, +}; +class StubIndexer extends Indexer { + result; + constructor(result) { + super(testConfig); + this.result = result; + } + async runOnce() { + return this.result; + } +} +describe("bounded CLI integer parsing", () => { + it("rejects partially numeric values instead of truncating them", () => { + expect(parseNonNegativeInteger("39381355", "to-height")).toBe(39381355); + expect(() => parseNonNegativeInteger("123abc", "to-height")).toThrow(/to-height must be a non-negative integer/i); + expect(() => parseNonNegativeInteger("-1", "to-height")).toThrow(/to-height must be a non-negative integer/i); + }); +}); +describe("bounded backfill completion", () => { + it("does not mark the range complete until the cursor reaches the requested max height", async () => { + const indexer = new StubIndexer({ processed: 20, head: 200, target: 150, cursorHeight: 120 }); + await expect(indexer.runUntilHeight(150)).resolves.toMatchObject({ processed: 20, cursorHeight: 120, done: false }); + }); + it("fails instead of silently succeeding when the confirmed target is below the requested max height", async () => { + const indexer = new StubIndexer({ processed: 0, head: 121, target: 119, cursorHeight: 119 }); + await expect(indexer.runUntilHeight(150)).rejects.toThrow(/confirmed target 119 is below requested to-height 150/i); + }); +}); +describe("nextBlockRange", () => { + it("returns an empty range when the confirmed target is behind the next cursor height", () => { + expect(nextBlockRange({ lastHeight: 100, confirmedTarget: 100, batchSize: 20 })).toEqual({ from: 101, to: 100, empty: true }); + }); + it("limits the next range by batch size", () => { + expect(nextBlockRange({ lastHeight: 100, confirmedTarget: 150, batchSize: 20 })).toEqual({ from: 101, to: 120, empty: false }); + }); + it("caps the next range at an explicit backfill end height", () => { + expect(nextBlockRange({ lastHeight: 100, confirmedTarget: 150, batchSize: 20, maxHeight: 110 })).toEqual({ from: 101, to: 110, empty: false }); + }); + it("returns empty after the explicit backfill end height has been reached", () => { + expect(nextBlockRange({ lastHeight: 110, confirmedTarget: 150, batchSize: 20, maxHeight: 110 })).toEqual({ from: 111, to: 110, empty: true }); + }); +}); diff --git a/indexer/dist/test/rpc.test.js b/indexer/dist/test/rpc.test.js new file mode 100644 index 000000000..0df777c35 --- /dev/null +++ b/indexer/dist/test/rpc.test.js @@ -0,0 +1,149 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { IndexerMetrics } from "../src/metrics.js"; +import { JunoRestClient, JunoRpcClient } from "../src/rpc.js"; +afterEach(() => vi.restoreAllMocks()); +describe("JunoRestClient", () => { + it("queries pair pool state at an explicit historical height", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ + data: { + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "123" }, + { info: { token: { contract_addr: "juno1token" } }, amount: "456" }, + ], + total_share: "789", + }, + }), + }); + const state = await new JunoRestClient("https://lcd.example").poolState("juno1pair", 39381355); + expect(state).toEqual({ reserves: [{ denom: "ujuno", amount: "123" }, { denom: "juno1token", amount: "456" }], totalShare: "789" }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [url, init] = fetchSpy.mock.calls[0] ?? []; + expect(String(url)).toMatch(/^https:\/\/lcd\.example\/cosmwasm\/wasm\/v1\/contract\/juno1pair\/smart\//); + const encoded = String(url).split("/smart/")[1] ?? ""; + expect(JSON.parse(Buffer.from(decodeURIComponent(encoded), "base64").toString("utf8"))).toEqual({ pool: {} }); + expect(init.headers).toMatchObject({ "x-cosmos-block-height": "39381355" }); + }); + it("rejects malformed pool responses instead of fabricating reserves", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, json: async () => ({ data: { assets: [] } }) }); + await expect(new JunoRestClient("https://lcd.example").poolState("juno1pair", 1)).rejects.toThrow(/no reserves/i); + }); +}); +describe("JunoRpcClient", () => { + it("retries transient RPC statuses and returns the successful response", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce(response(500, "Internal Server Error", {})) + .mockResolvedValueOnce(response(429, "Too Many Requests", {})) + .mockResolvedValueOnce(response(200, "OK", { + result: { sync_info: { latest_block_height: "123", latest_block_hash: "ABC" } }, + })); + const head = await new JunoRpcClient("https://rpc.example", { timeoutMs: 1_000, maxRetries: 2 }).head(); + expect(head).toEqual({ height: 123, hash: "ABC" }); + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(fetchSpy.mock.calls.map(([url]) => String(url))).toEqual([ + "https://rpc.example/status", + "https://rpc.example/status", + "https://rpc.example/status", + ]); + }); + it("honors timeout/retry options while recording RPC metrics", async () => { + const metrics = new IndexerMetrics(); + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce(response(503, "Service Unavailable", {})) + .mockResolvedValueOnce(response(200, "OK", { + result: { sync_info: { latest_block_height: "123", latest_block_hash: "ABC" } }, + })); + const head = await new JunoRpcClient("https://rpc.example", { timeoutMs: 1_000, maxRetries: 1, metrics }).head(); + expect(head).toEqual({ height: 123, hash: "ABC" }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + const init = fetchSpy.mock.calls[0]?.[1]; + expect(init?.signal).toBeInstanceOf(AbortSignal); + const snapshot = metrics.snapshot(); + expect(snapshot.rpcRequestsInFlight).toBe(0); + expect(snapshot.rpcErrors.get("503")).toBe(1); + }); + it("does not retry permanent RPC statuses and fails clearly", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(response(400, "Bad Request", {})); + await expect(new JunoRpcClient("https://rpc.example", { maxRetries: 3 }).head()).rejects.toThrow("RPC /status failed: 400 Bad Request"); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + it("fails clearly after transient statuses exhaust retries", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce(response(408, "Request Timeout", {})) + .mockResolvedValueOnce(response(503, "Service Unavailable", {})); + await expect(new JunoRpcClient("https://rpc.example", { maxRetries: 1 }).head()).rejects.toThrow("RPC /status failed: 503 Service Unavailable"); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + it("passes an AbortController signal to RPC fetches for timeout support", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(response(200, "OK", { + result: { sync_info: { latest_block_height: "5", latest_block_hash: "HASH" } }, + })); + await new JunoRpcClient("https://rpc.example", { timeoutMs: 50, maxRetries: 0 }).head(); + const init = fetchSpy.mock.calls[0]?.[1]; + expect(init?.signal).toBeInstanceOf(AbortSignal); + }); + it("treats aborted RPC requests as transient and retries", async () => { + const abortError = new Error("aborted"); + abortError.name = "AbortError"; + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockRejectedValueOnce(abortError) + .mockResolvedValueOnce(response(200, "OK", { + result: { sync_info: { latest_block_height: "6", latest_block_hash: "HASH6" } }, + })); + const head = await new JunoRpcClient("https://rpc.example", { timeoutMs: 50, maxRetries: 1 }).head(); + expect(head).toEqual({ height: 6, hash: "HASH6" }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + it("retries transient failures from block result fetches", async () => { + let blockResultsAttempts = 0; + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/block?height=9") + return response(200, "OK", blockResponse(9)); + if (url === "https://rpc.example/block_results?height=9") { + blockResultsAttempts += 1; + if (blockResultsAttempts === 1) + return response(503, "Service Unavailable", {}); + return response(200, "OK", { result: { txs_results: [] } }); + } + throw new Error(`unexpected URL ${url}`); + }); + const block = await new JunoRpcClient("https://rpc.example", { timeoutMs: 1_000, maxRetries: 1 }).block(9); + expect(block).toMatchObject({ height: 9, hash: "HASH9", txCount: 0, txEvents: [] }); + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(blockResultsAttempts).toBe(2); + }); + it("records fetched blocks only after both block RPC responses succeed", async () => { + const metrics = new IndexerMetrics(); + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/block?height=9") + return response(200, "OK", blockResponse(9)); + if (url === "https://rpc.example/block_results?height=9") + return response(500, "Internal Server Error", {}); + throw new Error(`unexpected URL ${url}`); + }); + await expect(new JunoRpcClient("https://rpc.example", { maxRetries: 0, metrics }).block(9)).rejects.toThrow("RPC /block_results?height=9 failed: 500 Internal Server Error"); + expect(metrics.snapshot().fetchBlocksTotal).toBe(0); + }); +}); +function blockResponse(height) { + return { + result: { + block_id: { hash: `HASH${height}` }, + block: { + header: { time: "2026-01-01T00:00:00Z", last_block_id: { hash: `HASH${height - 1}` } }, + data: { txs: [] }, + }, + }, + }; +} +function response(status, statusText, body) { + return { + ok: status >= 200 && status < 300, + status, + statusText, + json: async () => body, + }; +} diff --git a/indexer/dist/test/snapshot-worker.test.js b/indexer/dist/test/snapshot-worker.test.js new file mode 100644 index 000000000..2255d8df5 --- /dev/null +++ b/indexer/dist/test/snapshot-worker.test.js @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_CONTRACTS } from "../src/config.js"; +import { isPermanentSnapshotFailure, SnapshotWorker } from "../src/snapshot-worker.js"; +class FakeSnapshotClient { + queries = []; + jobRows = [{ id: "1", chain_id: "juno-1", pair_address: "juno1pair", height: "39381355", block_time: "2026-07-01T03:01:00Z", reason: "touched", status: "leased", attempts: 1 }]; + async query(text, values) { + this.queries.push({ text, values }); + if (text.includes("WITH claimable")) + return { rows: this.jobRows, rowCount: this.jobRows.length }; + if (text.includes("FROM pools") && text.includes("pair_address")) + return { rows: [{ id: "pool-1" }], rowCount: 1 }; + if (text.includes("INSERT INTO pool_state_snapshots")) + return { rows: [], rowCount: 1 }; + if (text.includes("UPDATE snapshot_jobs")) + return { rows: [], rowCount: 1 }; + return { rows: [], rowCount: 0 }; + } + release() { } +} +class FakeSnapshotPool { + client = new FakeSnapshotClient(); + async connect() { return this.client; } +} +const config = { + databaseUrl: "postgres://test", + rpcUrl: "https://rpc.example", + restUrl: "https://lcd.example", + wsUrl: "wss://rpc.example/websocket", + chainId: "juno-1", + factoryAddress: DEFAULT_CONTRACTS.factory, + routerAddress: DEFAULT_CONTRACTS.router, + incentivesAddress: DEFAULT_CONTRACTS.incentives, + oracleAddress: DEFAULT_CONTRACTS.oracle, + nativeCoinRegistryAddress: DEFAULT_CONTRACTS.nativeCoinRegistry, + startHeight: 11, + confirmationDepth: 2, + pollIntervalMs: 1, + batchSize: 1, + dryRun: false, + cursorId: "astroport-juno-v1", + indexerMode: "realtime", + rangeSize: 5_000, + fetchWindowSize: 250, + fetchConcurrency: 32, + realtimeFetchConcurrency: 8, + rpcTimeoutMs: 10_000, + rpcMaxRetries: 5, + ingestCandlesInline: true, + ingestReserveSnapshotsInline: false, + ingestAggregatesInline: false, + ingestBulkStagingEnabled: false, + priceProviderName: "provider", + priceCacheTtlMs: 300_000, + priceStaleAfterMs: 1_800_000, + priceAllowStale: true, + priceDevMocks: false, + readModelRefreshIntervalMs: 0, + apiPort: 8787, +}; +describe("SnapshotWorker", () => { + it("processes a claimed job by querying LCD at height, writing a snapshot, and marking success", async () => { + const pool = new FakeSnapshotPool(); + const rest = { + poolState: async (pairAddress, height) => { + expect(pairAddress).toBe("juno1pair"); + expect(height).toBe(39381355); + return { reserves: [{ denom: "ujuno", amount: "123" }], totalShare: "456" }; + }, + }; + await expect(new SnapshotWorker(config, pool, rest, { batchSize: 10, leaseSeconds: 30, maxAttempts: 3 }).processBatch()).resolves.toBe(1); + const claim = pool.client.queries.find((query) => query.text.includes("FOR UPDATE SKIP LOCKED")); + expect(claim?.values).toEqual(["juno-1", 10, "30 seconds", 3]); + const snapshot = pool.client.queries.find((query) => query.text.includes("INSERT INTO pool_state_snapshots")); + expect(snapshot?.values).toEqual(["pool-1", 39381355, "2026-07-01T03:01:00Z", JSON.stringify([{ denom: "ujuno", amount: "123" }]), "456", "lcd"]); + expect(pool.client.queries.some((query) => query.text.includes("status = 'succeeded'"))).toBe(true); + }); + it("retries transient LCD failures by returning the job to pending", async () => { + const pool = new FakeSnapshotPool(); + const rest = { poolState: async () => { throw new Error("LCD smart query failed: 500 unavailable"); } }; + await expect(new SnapshotWorker(config, pool, rest, { maxAttempts: 5 }).processBatch()).resolves.toBe(1); + const failure = pool.client.queries.find((query) => query.text.includes("last_error = $4")); + expect(failure?.values).toEqual(["1", 1, false, "LCD smart query failed: 500 unavailable", 5]); + }); + it("marks permanent failures without retrying", async () => { + const pool = new FakeSnapshotPool(); + const rest = { poolState: async () => { throw new Error("LCD smart query failed: 404 Not Found"); } }; + await expect(new SnapshotWorker(config, pool, rest, { maxAttempts: 5 }).processBatch()).resolves.toBe(1); + const failure = pool.client.queries.find((query) => query.text.includes("last_error = $4")); + expect(failure?.values).toEqual(["1", 1, true, "LCD smart query failed: 404 Not Found", 5]); + expect(isPermanentSnapshotFailure(new Error("LCD smart query failed: 404 Not Found"))).toBe(true); + expect(isPermanentSnapshotFailure(new Error("LCD smart query failed: 429 Too Many Requests"))).toBe(false); + }); +}); diff --git a/indexer/docker-compose.yml b/indexer/docker-compose.yml new file mode 100644 index 000000000..38888e8e5 --- /dev/null +++ b/indexer/docker-compose.yml @@ -0,0 +1,30 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: astroport_indexer + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - indexer-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d astroport_indexer"] + interval: 5s + timeout: 5s + retries: 10 + + indexer: + build: . + env_file: + - .env.example + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/astroport_indexer + profiles: ["indexer"] + +volumes: + indexer-postgres: diff --git a/indexer/migrations/001_init.sql b/indexer/migrations/001_init.sql new file mode 100644 index 000000000..609f59117 --- /dev/null +++ b/indexer/migrations/001_init.sql @@ -0,0 +1,182 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE IF NOT EXISTS schema_migrations ( + version TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS indexer_cursors ( + id TEXT PRIMARY KEY, + chain_id TEXT NOT NULL, + last_height BIGINT NOT NULL DEFAULT 0, + last_block_hash TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS processed_blocks ( + height BIGINT PRIMARY KEY, + chain_id TEXT NOT NULL, + block_hash TEXT NOT NULL, + parent_hash TEXT, + block_time TIMESTAMPTZ NOT NULL, + tx_count INTEGER NOT NULL DEFAULT 0, + processed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS pools ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chain_id TEXT NOT NULL, + pair_address TEXT NOT NULL, + factory_address TEXT NOT NULL, + liquidity_token_address TEXT, + pool_type TEXT, + asset_infos JSONB NOT NULL DEFAULT '[]'::jsonb, + created_height BIGINT, + created_tx_hash TEXT, + first_seen_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (chain_id, pair_address) +); +CREATE INDEX IF NOT EXISTS pools_factory_idx ON pools (factory_address); +CREATE INDEX IF NOT EXISTS pools_assets_gin_idx ON pools USING gin (asset_infos); + +CREATE TABLE IF NOT EXISTS pool_state_snapshots ( + id BIGSERIAL PRIMARY KEY, + pool_id UUID NOT NULL REFERENCES pools(id) ON DELETE CASCADE, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + reserves JSONB NOT NULL DEFAULT '[]'::jsonb, + total_share NUMERIC(78,0), + tvl_usd NUMERIC(38,12), + source TEXT NOT NULL DEFAULT 'event', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (pool_id, height, source) +); +CREATE INDEX IF NOT EXISTS pool_state_pool_height_idx ON pool_state_snapshots (pool_id, height DESC); + +CREATE TABLE IF NOT EXISTS swaps ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + pool_id UUID REFERENCES pools(id) ON DELETE SET NULL, + pair_address TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + trader TEXT, + offer_asset TEXT, + offer_amount NUMERIC(78,0), + ask_asset TEXT, + return_amount NUMERIC(78,0), + spread_amount NUMERIC(78,0), + commission_amount NUMERIC(78,0), + raw_event JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS swaps_pool_height_idx ON swaps (pair_address, height DESC); +CREATE INDEX IF NOT EXISTS swaps_block_time_idx ON swaps (block_time DESC); +CREATE INDEX IF NOT EXISTS swaps_pair_time_order_idx ON swaps (chain_id, pair_address, block_time, height, msg_index, event_index, id); +CREATE UNIQUE INDEX IF NOT EXISTS swaps_idempotency_idx + ON swaps (chain_id, tx_hash, msg_index, event_index, pair_address, COALESCE(trader, '')); + +DO $$ +BEGIN + CREATE TYPE liquidity_event_kind AS ENUM ('provide', 'withdraw'); +EXCEPTION + WHEN duplicate_object THEN NULL; +END +$$; + +CREATE TABLE IF NOT EXISTS liquidity_events ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + pool_id UUID REFERENCES pools(id) ON DELETE SET NULL, + pair_address TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + kind liquidity_event_kind NOT NULL, + provider TEXT, + assets JSONB NOT NULL DEFAULT '[]'::jsonb, + share_amount NUMERIC(78,0), + raw_event JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS liquidity_events_pool_height_idx ON liquidity_events (pair_address, height DESC); +CREATE INDEX IF NOT EXISTS liquidity_events_provider_idx ON liquidity_events (provider); +CREATE UNIQUE INDEX IF NOT EXISTS liquidity_events_idempotency_idx + ON liquidity_events (chain_id, tx_hash, msg_index, event_index, pair_address, kind, COALESCE(provider, '')); + +CREATE TABLE IF NOT EXISTS incentive_events ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + incentives_address TEXT NOT NULL, + lp_token_address TEXT, + user_address TEXT, + action TEXT NOT NULL, + amount NUMERIC(78,0), + reward_asset TEXT, + reward_amount NUMERIC(78,0), + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + raw_event JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS incentive_events_user_idx ON incentive_events (user_address); +CREATE INDEX IF NOT EXISTS incentive_events_lp_idx ON incentive_events (lp_token_address); +CREATE UNIQUE INDEX IF NOT EXISTS incentive_events_idempotency_idx + ON incentive_events (chain_id, tx_hash, msg_index, event_index, incentives_address, action, COALESCE(user_address, '')); + +CREATE TABLE IF NOT EXISTS positions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chain_id TEXT NOT NULL, + pool_id UUID REFERENCES pools(id) ON DELETE CASCADE, + pair_address TEXT NOT NULL, + owner_address TEXT NOT NULL, + lp_token_address TEXT, + lp_balance NUMERIC(78,0) NOT NULL DEFAULT 0, + bonded_balance NUMERIC(78,0) NOT NULL DEFAULT 0, + last_height BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (chain_id, pair_address, owner_address) +); +CREATE INDEX IF NOT EXISTS positions_owner_idx ON positions (owner_address); + +CREATE TABLE IF NOT EXISTS token_prices ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + asset TEXT NOT NULL, + price_usd NUMERIC(38,18) NOT NULL, + source TEXT NOT NULL, + height BIGINT, + block_time TIMESTAMPTZ, + observed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (chain_id, asset, source, observed_at) +); +CREATE INDEX IF NOT EXISTS token_prices_asset_time_idx ON token_prices (asset, observed_at DESC); + +CREATE TABLE IF NOT EXISTS token_candles ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + asset TEXT NOT NULL, + quote_asset TEXT NOT NULL DEFAULT 'uusd', + interval TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + open NUMERIC(38,18) NOT NULL, + high NUMERIC(38,18) NOT NULL, + low NUMERIC(38,18) NOT NULL, + close NUMERIC(38,18) NOT NULL, + volume NUMERIC(78,0) NOT NULL DEFAULT 0, + volume_usd NUMERIC(38,12), + trade_count INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (chain_id, asset, quote_asset, interval, bucket_start) +); +CREATE INDEX IF NOT EXISTS token_candles_asset_interval_idx ON token_candles (asset, interval, bucket_start DESC); diff --git a/indexer/migrations/002_pool_candles.sql b/indexer/migrations/002_pool_candles.sql new file mode 100644 index 000000000..85ecb079b --- /dev/null +++ b/indexer/migrations/002_pool_candles.sql @@ -0,0 +1,20 @@ +ALTER TABLE token_candles + ADD COLUMN IF NOT EXISTS pool_id UUID REFERENCES pools(id) ON DELETE CASCADE, + ADD COLUMN IF NOT EXISTS pair_address TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS source TEXT NOT NULL DEFAULT 'indexer'; + +UPDATE token_candles tc +SET pool_id = p.id +FROM pools p +WHERE tc.pool_id IS NULL + AND tc.pair_address <> '' + AND p.chain_id = tc.chain_id + AND p.pair_address = tc.pair_address; + +DROP INDEX IF EXISTS token_candles_asset_interval_idx; +ALTER TABLE token_candles DROP CONSTRAINT IF EXISTS token_candles_chain_id_asset_quote_asset_interval_bucket_start_key; + +CREATE UNIQUE INDEX IF NOT EXISTS token_candles_pool_asset_interval_bucket_uq + ON token_candles (chain_id, pair_address, asset, quote_asset, interval, bucket_start); +CREATE INDEX IF NOT EXISTS token_candles_pool_interval_idx ON token_candles (pair_address, interval, bucket_start DESC); +CREATE INDEX IF NOT EXISTS token_candles_asset_interval_idx ON token_candles (asset, quote_asset, interval, bucket_start DESC); diff --git a/indexer/migrations/003_api_pricing_readiness.sql b/indexer/migrations/003_api_pricing_readiness.sql new file mode 100644 index 000000000..4ad95ffc4 --- /dev/null +++ b/indexer/migrations/003_api_pricing_readiness.sql @@ -0,0 +1,56 @@ +ALTER TABLE token_prices ALTER COLUMN price_usd DROP NOT NULL; +ALTER TABLE token_prices ADD COLUMN IF NOT EXISTS price_juno NUMERIC(38,18); +ALTER TABLE token_prices ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'fresh'; +ALTER TABLE token_prices ADD COLUMN IF NOT EXISTS stale_after TIMESTAMPTZ; +ALTER TABLE token_prices ADD COLUMN IF NOT EXISTS raw_payload JSONB; + +CREATE INDEX IF NOT EXISTS token_prices_status_idx ON token_prices (chain_id, asset, status, observed_at DESC); + +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS tvl_juno NUMERIC(38,12); +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS volume_24h_usd NUMERIC(38,12); +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS volume_24h_juno NUMERIC(38,12); +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS volume_7d_usd NUMERIC(38,12); +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS volume_7d_juno NUMERIC(38,12); +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS fees_24h_usd NUMERIC(38,12); +ALTER TABLE pool_state_snapshots ADD COLUMN IF NOT EXISTS fees_24h_juno NUMERIC(38,12); + +ALTER TABLE token_candles ADD COLUMN IF NOT EXISTS volume_quote NUMERIC(78,18); + +CREATE TABLE IF NOT EXISTS asset_metadata ( + chain_id TEXT NOT NULL, + asset TEXT NOT NULL, + symbol TEXT, + decimals INTEGER, + logo_uri TEXT, + verified BOOLEAN NOT NULL DEFAULT false, + ibc_trace JSONB, + source TEXT NOT NULL DEFAULT 'registry', + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (chain_id, asset) +); + +DROP VIEW IF EXISTS latest_pool_states; +CREATE VIEW latest_pool_states AS +SELECT DISTINCT ON (p.chain_id, p.pair_address) + p.chain_id, + p.id AS pool_id, + p.pair_address, + p.liquidity_token_address, + p.pool_type, + p.asset_infos, + s.height, + s.block_time, + s.reserves, + s.total_share, + s.tvl_usd, + s.tvl_juno, + s.volume_24h_usd, + s.volume_24h_juno, + s.volume_7d_usd, + s.volume_7d_juno, + s.fees_24h_usd, + s.fees_24h_juno, + s.created_at AS state_updated_at +FROM pools p +LEFT JOIN pool_state_snapshots s ON s.pool_id = p.id +ORDER BY p.chain_id, p.pair_address, s.height DESC NULLS LAST; diff --git a/indexer/migrations/004_pool_state_source_precedence.sql b/indexer/migrations/004_pool_state_source_precedence.sql new file mode 100644 index 000000000..59d55e3a8 --- /dev/null +++ b/indexer/migrations/004_pool_state_source_precedence.sql @@ -0,0 +1,35 @@ +DROP VIEW IF EXISTS latest_pool_states; +CREATE VIEW latest_pool_states AS +SELECT DISTINCT ON (p.chain_id, p.pair_address) + p.chain_id, + p.id AS pool_id, + p.pair_address, + p.liquidity_token_address, + p.pool_type, + p.asset_infos, + p.created_height, + p.created_tx_hash, + p.first_seen_at, + p.updated_at, + s.height, + s.block_time, + s.reserves, + s.total_share, + s.tvl_usd, + s.tvl_juno, + s.volume_24h_usd, + s.volume_24h_juno, + s.volume_7d_usd, + s.volume_7d_juno, + s.fees_24h_usd, + s.fees_24h_juno, + s.created_at AS state_updated_at +FROM pools p +LEFT JOIN pool_state_snapshots s ON s.pool_id = p.id +ORDER BY p.chain_id, p.pair_address, s.height DESC NULLS LAST, + CASE s.source + WHEN 'lcd' THEN 0 + WHEN 'event' THEN 1 + ELSE 2 + END, + s.created_at DESC; diff --git a/indexer/migrations/005_snapshot_jobs.sql b/indexer/migrations/005_snapshot_jobs.sql new file mode 100644 index 000000000..eb87bce9b --- /dev/null +++ b/indexer/migrations/005_snapshot_jobs.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS snapshot_jobs ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + pair_address TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + reason TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'leased', 'succeeded', 'failed')), + attempts INTEGER NOT NULL DEFAULT 0, + leased_until TIMESTAMPTZ, + last_error TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (chain_id, pair_address, height, reason) +); + +CREATE INDEX IF NOT EXISTS snapshot_jobs_claim_idx + ON snapshot_jobs (status, leased_until, id) + WHERE status IN ('pending', 'leased'); +CREATE INDEX IF NOT EXISTS snapshot_jobs_pair_height_idx + ON snapshot_jobs (chain_id, pair_address, height DESC); diff --git a/indexer/migrations/006_candle_jobs.sql b/indexer/migrations/006_candle_jobs.sql new file mode 100644 index 000000000..3434bf4e3 --- /dev/null +++ b/indexer/migrations/006_candle_jobs.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS candle_jobs ( + id BIGSERIAL PRIMARY KEY, + chain_id TEXT NOT NULL, + pair_address TEXT NOT NULL, + from_time TIMESTAMPTZ NOT NULL, + to_time TIMESTAMPTZ NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + attempts INTEGER NOT NULL DEFAULT 0, + worker_id TEXT, + last_error TEXT, + claimed_at TIMESTAMPTZ, + run_after TIMESTAMPTZ NOT NULL DEFAULT now(), + processed_swaps INTEGER NOT NULL DEFAULT 0, + rerun_requested BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CHECK (status IN ('pending', 'running', 'completed', 'failed')), + CHECK (to_time > from_time), + UNIQUE (chain_id, pair_address, from_time, to_time) +); + +CREATE INDEX IF NOT EXISTS candle_jobs_claim_idx + ON candle_jobs (status, run_after, created_at); +CREATE INDEX IF NOT EXISTS candle_jobs_pair_range_idx + ON candle_jobs (chain_id, pair_address, from_time, to_time); + +CREATE INDEX IF NOT EXISTS swaps_pair_time_order_idx + ON swaps (chain_id, pair_address, block_time, height, msg_index, event_index, id); \ No newline at end of file diff --git a/indexer/migrations/007_bulk_staging.sql b/indexer/migrations/007_bulk_staging.sql new file mode 100644 index 000000000..781d90048 --- /dev/null +++ b/indexer/migrations/007_bulk_staging.sql @@ -0,0 +1,98 @@ +CREATE TABLE IF NOT EXISTS stage_processed_blocks ( + batch_id UUID NOT NULL, + chain_id TEXT NOT NULL, + height BIGINT NOT NULL, + block_hash TEXT NOT NULL, + parent_hash TEXT, + block_time TIMESTAMPTZ NOT NULL, + tx_count INTEGER NOT NULL DEFAULT 0, + staged_at TIMESTAMPTZ NOT NULL DEFAULT now(), + merged_at TIMESTAMPTZ, + PRIMARY KEY (batch_id, chain_id, height) +); +CREATE INDEX IF NOT EXISTS stage_processed_blocks_chain_height_idx ON stage_processed_blocks (chain_id, height); +CREATE INDEX IF NOT EXISTS stage_processed_blocks_merged_at_idx ON stage_processed_blocks (merged_at) WHERE merged_at IS NOT NULL; + +CREATE TABLE IF NOT EXISTS stage_pools ( + batch_id UUID NOT NULL, + chain_id TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + factory_address TEXT NOT NULL, + pair_address TEXT NOT NULL, + liquidity_token_address TEXT, + pool_type TEXT, + asset_infos JSONB NOT NULL DEFAULT '[]'::jsonb, + raw_event JSONB NOT NULL, + staged_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (batch_id, chain_id, height, tx_hash, msg_index, event_index, pair_address) +); +CREATE INDEX IF NOT EXISTS stage_pools_batch_idx ON stage_pools (batch_id, chain_id, pair_address); + +CREATE TABLE IF NOT EXISTS stage_swaps ( + batch_id UUID NOT NULL, + chain_id TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + pair_address TEXT NOT NULL, + trader TEXT, + offer_asset TEXT, + offer_amount NUMERIC(78,0), + ask_asset TEXT, + return_amount NUMERIC(78,0), + spread_amount NUMERIC(78,0), + commission_amount NUMERIC(78,0), + raw_event JSONB NOT NULL, + staged_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS stage_swaps_batch_idx ON stage_swaps (batch_id, chain_id, pair_address); +CREATE UNIQUE INDEX IF NOT EXISTS stage_swaps_idempotency_idx + ON stage_swaps (batch_id, chain_id, height, tx_hash, msg_index, event_index, pair_address, COALESCE(trader, '')); + +CREATE TABLE IF NOT EXISTS stage_liquidity_events ( + batch_id UUID NOT NULL, + chain_id TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + pair_address TEXT NOT NULL, + kind liquidity_event_kind NOT NULL, + provider TEXT, + assets JSONB NOT NULL DEFAULT '[]'::jsonb, + share_amount NUMERIC(78,0), + raw_event JSONB NOT NULL, + staged_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS stage_liquidity_events_batch_idx ON stage_liquidity_events (batch_id, chain_id, pair_address); +CREATE UNIQUE INDEX IF NOT EXISTS stage_liquidity_events_idempotency_idx + ON stage_liquidity_events (batch_id, chain_id, height, tx_hash, msg_index, event_index, pair_address, kind, COALESCE(provider, '')); + +CREATE TABLE IF NOT EXISTS stage_incentive_events ( + batch_id UUID NOT NULL, + chain_id TEXT NOT NULL, + height BIGINT NOT NULL, + block_time TIMESTAMPTZ NOT NULL, + tx_hash TEXT NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + incentives_address TEXT NOT NULL, + lp_token_address TEXT, + user_address TEXT, + action TEXT NOT NULL, + amount NUMERIC(78,0), + reward_asset TEXT, + reward_amount NUMERIC(78,0), + raw_event JSONB NOT NULL, + staged_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS stage_incentive_events_batch_idx ON stage_incentive_events (batch_id, chain_id, incentives_address); +CREATE UNIQUE INDEX IF NOT EXISTS stage_incentive_events_idempotency_idx + ON stage_incentive_events (batch_id, chain_id, height, tx_hash, msg_index, event_index, incentives_address, action, COALESCE(user_address, '')); diff --git a/indexer/migrations/008_read_models.sql b/indexer/migrations/008_read_models.sql new file mode 100644 index 000000000..1f9b9b723 --- /dev/null +++ b/indexer/migrations/008_read_models.sql @@ -0,0 +1,415 @@ +-- API read models for high-traffic endpoints. These are ordinary tables plus +-- explicit refresh helpers so deployments do not require TimescaleDB or external +-- USD price providers. + +CREATE TABLE IF NOT EXISTS latest_pool_state ( + chain_id TEXT NOT NULL, + pool_id UUID NOT NULL, + pair_address TEXT NOT NULL, + liquidity_token_address TEXT, + pool_type TEXT, + asset_infos JSONB NOT NULL DEFAULT '[]'::jsonb, + created_height BIGINT, + created_tx_hash TEXT, + first_seen_at TIMESTAMPTZ, + pool_updated_at TIMESTAMPTZ, + state_height BIGINT, + state_block_time TIMESTAMPTZ, + reserves JSONB NOT NULL DEFAULT '[]'::jsonb, + total_share NUMERIC(78,0), + tvl_usd NUMERIC(38,12), + tvl_juno NUMERIC(38,12), + volume_24h_usd NUMERIC(38,12), + volume_24h_juno NUMERIC(38,12), + volume_7d_usd NUMERIC(38,12), + volume_7d_juno NUMERIC(38,12), + fees_24h_usd NUMERIC(38,12), + fees_24h_juno NUMERIC(38,12), + snapshot_source TEXT, + state_updated_at TIMESTAMPTZ, + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (chain_id, pair_address) +); +CREATE INDEX IF NOT EXISTS latest_pool_state_tvl_idx ON latest_pool_state (chain_id, tvl_usd DESC NULLS LAST, created_height DESC NULLS LAST); +CREATE UNIQUE INDEX IF NOT EXISTS latest_pool_state_pool_uq ON latest_pool_state (chain_id, pool_id); + +CREATE TABLE IF NOT EXISTS pool_volume_windows ( + chain_id TEXT NOT NULL, + pool_id UUID NOT NULL, + pair_address TEXT NOT NULL, + time_window TEXT NOT NULL CHECK (time_window IN ('24h', '7d')), + volume_usd NUMERIC(38,12), + volume_juno NUMERIC(38,12), + fees_usd NUMERIC(38,12), + fees_juno NUMERIC(38,12), + swap_count INTEGER NOT NULL DEFAULT 0, + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (chain_id, pair_address, time_window) +); +CREATE INDEX IF NOT EXISTS pool_volume_windows_chain_window_idx ON pool_volume_windows (chain_id, time_window, volume_usd DESC NULLS LAST); + +CREATE TABLE IF NOT EXISTS pool_candle_buckets ( + chain_id TEXT NOT NULL, + pool_id UUID, + pair_address TEXT NOT NULL, + asset TEXT NOT NULL, + quote_asset TEXT NOT NULL, + interval TEXT NOT NULL, + bucket_start TIMESTAMPTZ NOT NULL, + open NUMERIC(38,18) NOT NULL, + high NUMERIC(38,18) NOT NULL, + low NUMERIC(38,18) NOT NULL, + close NUMERIC(38,18) NOT NULL, + volume NUMERIC(78,18) NOT NULL DEFAULT 0, + volume_quote NUMERIC(78,18), + volume_usd NUMERIC(38,12), + trade_count INTEGER NOT NULL DEFAULT 0, + source TEXT NOT NULL DEFAULT 'indexer', + updated_at TIMESTAMPTZ, + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (chain_id, pair_address, asset, quote_asset, interval, bucket_start) +); +CREATE INDEX IF NOT EXISTS pool_candle_buckets_api_idx ON pool_candle_buckets (chain_id, pair_address, interval, bucket_start DESC); +CREATE INDEX IF NOT EXISTS pool_candle_buckets_asset_idx ON pool_candle_buckets (chain_id, asset, quote_asset, interval, bucket_start DESC); + +CREATE TABLE IF NOT EXISTS wallet_history_flat ( + chain_id TEXT NOT NULL, + tx_hash TEXT NOT NULL, + wallet_address TEXT NOT NULL, + pair_address TEXT, + type TEXT NOT NULL, + height BIGINT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + msg_index INTEGER NOT NULL DEFAULT 0, + event_index INTEGER NOT NULL DEFAULT 0, + offer_asset JSONB, + ask_asset JSONB, + amount_usd NUMERIC(38,12), + fee_usd NUMERIC(38,12), + success BOOLEAN NOT NULL DEFAULT true, + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (chain_id, wallet_address, tx_hash, type, msg_index, event_index) +); +CREATE INDEX IF NOT EXISTS wallet_history_flat_api_idx ON wallet_history_flat (chain_id, wallet_address, height DESC, timestamp DESC); + +CREATE TABLE IF NOT EXISTS wallet_position_latest ( + chain_id TEXT NOT NULL, + wallet_address TEXT NOT NULL, + pool_id UUID, + pair_address TEXT NOT NULL, + lp_token_address TEXT, + lp_balance NUMERIC(78,0) NOT NULL DEFAULT 0, + bonded_balance NUMERIC(78,0) NOT NULL DEFAULT 0, + last_height BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (chain_id, wallet_address, pair_address) +); +CREATE INDEX IF NOT EXISTS wallet_position_latest_pool_idx ON wallet_position_latest (chain_id, pair_address, updated_at DESC); + +CREATE TABLE IF NOT EXISTS protocol_stats_latest ( + chain_id TEXT PRIMARY KEY, + pool_count INTEGER NOT NULL DEFAULT 0, + incentivized_pools INTEGER NOT NULL DEFAULT 0, + tvl_usd NUMERIC(38,12), + tvl_juno NUMERIC(38,12), + volume_24h_usd NUMERIC(38,12), + volume_24h_juno NUMERIC(38,12), + volume_7d_usd NUMERIC(38,12), + volume_7d_juno NUMERIC(38,12), + fees_24h_usd NUMERIC(38,12), + fees_24h_juno NUMERIC(38,12), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE OR REPLACE FUNCTION refresh_latest_pool_state(target_chain_id TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + WITH latest_snapshot AS ( + SELECT DISTINCT ON (p.chain_id, p.pair_address) + p.chain_id, p.id AS pool_id, p.pair_address, p.liquidity_token_address, p.pool_type, + p.asset_infos, p.created_height, p.created_tx_hash, p.first_seen_at, p.updated_at AS pool_updated_at, + s.height AS state_height, s.block_time AS state_block_time, COALESCE(s.reserves, '[]'::jsonb) AS reserves, + s.total_share, s.tvl_usd, COALESCE(s.tvl_juno, reserve_values.tvl_juno) AS tvl_juno, + s.volume_24h_usd, COALESCE(s.volume_24h_juno, swap_values.volume_24h_juno) AS volume_24h_juno, + s.volume_7d_usd, COALESCE(s.volume_7d_juno, swap_values.volume_7d_juno) AS volume_7d_juno, + s.fees_24h_usd, COALESCE(s.fees_24h_juno, swap_values.fees_24h_juno) AS fees_24h_juno, + s.source AS snapshot_source, s.created_at AS state_updated_at + FROM pools p + LEFT JOIN pool_state_snapshots s ON s.pool_id = p.id + LEFT JOIN LATERAL ( + SELECT sum((reserve->>'amount')::numeric) / 1000000 AS tvl_juno + FROM jsonb_array_elements(COALESCE(s.reserves, '[]'::jsonb)) reserve + WHERE reserve->>'denom' = 'ujuno' + AND reserve->>'amount' ~ '^[0-9]+(\.[0-9]+)?$' + ) reserve_values ON true + LEFT JOIN LATERAL ( + SELECT + sum(CASE + WHEN sw.block_time >= now() - interval '24 hours' AND sw.offer_asset = 'ujuno' THEN sw.offer_amount + WHEN sw.block_time >= now() - interval '24 hours' AND sw.ask_asset = 'ujuno' THEN sw.return_amount + ELSE NULL + END) / 1000000 AS volume_24h_juno, + sum(CASE + WHEN sw.offer_asset = 'ujuno' THEN sw.offer_amount + WHEN sw.ask_asset = 'ujuno' THEN sw.return_amount + ELSE NULL + END) / 1000000 AS volume_7d_juno, + sum(CASE + WHEN sw.block_time >= now() - interval '24 hours' AND sw.ask_asset = 'ujuno' THEN sw.commission_amount + ELSE NULL + END) / 1000000 AS fees_24h_juno + FROM swaps sw + WHERE sw.chain_id = p.chain_id + AND sw.pair_address = p.pair_address + AND sw.block_time >= now() - interval '7 days' + ) swap_values ON true + WHERE target_chain_id IS NULL OR p.chain_id = target_chain_id + ORDER BY p.chain_id, p.pair_address, s.height DESC NULLS LAST, + CASE s.source WHEN 'lcd' THEN 0 WHEN 'event' THEN 1 ELSE 2 END, + s.created_at DESC + ) + INSERT INTO latest_pool_state( + chain_id, pool_id, pair_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, + first_seen_at, pool_updated_at, state_height, state_block_time, reserves, total_share, tvl_usd, tvl_juno, + volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, + snapshot_source, state_updated_at, refreshed_at + ) + SELECT chain_id, pool_id, pair_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, + first_seen_at, pool_updated_at, state_height, state_block_time, reserves, total_share, tvl_usd, tvl_juno, + volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, + snapshot_source, state_updated_at, now() + FROM latest_snapshot + ON CONFLICT (chain_id, pair_address) DO UPDATE SET + pool_id = EXCLUDED.pool_id, + liquidity_token_address = EXCLUDED.liquidity_token_address, + pool_type = EXCLUDED.pool_type, + asset_infos = EXCLUDED.asset_infos, + created_height = EXCLUDED.created_height, + created_tx_hash = EXCLUDED.created_tx_hash, + first_seen_at = EXCLUDED.first_seen_at, + pool_updated_at = EXCLUDED.pool_updated_at, + state_height = EXCLUDED.state_height, + state_block_time = EXCLUDED.state_block_time, + reserves = EXCLUDED.reserves, + total_share = EXCLUDED.total_share, + tvl_usd = EXCLUDED.tvl_usd, + tvl_juno = EXCLUDED.tvl_juno, + volume_24h_usd = EXCLUDED.volume_24h_usd, + volume_24h_juno = EXCLUDED.volume_24h_juno, + volume_7d_usd = EXCLUDED.volume_7d_usd, + volume_7d_juno = EXCLUDED.volume_7d_juno, + fees_24h_usd = EXCLUDED.fees_24h_usd, + fees_24h_juno = EXCLUDED.fees_24h_juno, + snapshot_source = EXCLUDED.snapshot_source, + state_updated_at = EXCLUDED.state_updated_at, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; + +CREATE OR REPLACE FUNCTION refresh_pool_volume_windows(target_chain_id TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + WITH windows AS ( + SELECT chain_id, pool_id, pair_address, '24h'::text AS time_window, + volume_24h_usd AS volume_usd, volume_24h_juno AS volume_juno, + fees_24h_usd AS fees_usd, fees_24h_juno AS fees_juno + FROM latest_pool_state + WHERE target_chain_id IS NULL OR chain_id = target_chain_id + UNION ALL + SELECT chain_id, pool_id, pair_address, '7d'::text AS time_window, + volume_7d_usd, volume_7d_juno, NULL::numeric, NULL::numeric + FROM latest_pool_state + WHERE target_chain_id IS NULL OR chain_id = target_chain_id + ), counts AS ( + SELECT lps.chain_id, lps.pair_address, w.time_window, count(s.id)::int AS swap_count + FROM latest_pool_state lps + CROSS JOIN (VALUES ('24h'), ('7d')) AS w(time_window) + LEFT JOIN swaps s ON s.chain_id = lps.chain_id + AND s.pair_address = lps.pair_address + AND s.block_time >= now() - CASE w.time_window WHEN '24h' THEN interval '24 hours' ELSE interval '7 days' END + WHERE target_chain_id IS NULL OR lps.chain_id = target_chain_id + GROUP BY lps.chain_id, lps.pair_address, w.time_window + ) + INSERT INTO pool_volume_windows(chain_id, pool_id, pair_address, time_window, volume_usd, volume_juno, fees_usd, fees_juno, swap_count, refreshed_at) + SELECT w.chain_id, w.pool_id, w.pair_address, w.time_window, w.volume_usd, w.volume_juno, w.fees_usd, w.fees_juno, COALESCE(c.swap_count, 0), now() + FROM windows w + LEFT JOIN counts c ON c.chain_id = w.chain_id AND c.pair_address = w.pair_address AND c.time_window = w.time_window + ON CONFLICT (chain_id, pair_address, time_window) DO UPDATE SET + pool_id = EXCLUDED.pool_id, + volume_usd = EXCLUDED.volume_usd, + volume_juno = EXCLUDED.volume_juno, + fees_usd = EXCLUDED.fees_usd, + fees_juno = EXCLUDED.fees_juno, + swap_count = EXCLUDED.swap_count, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; + +CREATE OR REPLACE FUNCTION refresh_pool_candle_buckets(target_chain_id TEXT DEFAULT NULL, target_pair_address TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + INSERT INTO pool_candle_buckets(chain_id, pool_id, pair_address, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, volume_usd, trade_count, source, updated_at, refreshed_at) + SELECT chain_id, pool_id, pair_address, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, + volume_quote, volume_usd, trade_count, source, updated_at, now() + FROM token_candles + WHERE pair_address <> '' + AND (target_chain_id IS NULL OR chain_id = target_chain_id) + AND (target_pair_address IS NULL OR pair_address = target_pair_address) + ON CONFLICT (chain_id, pair_address, asset, quote_asset, interval, bucket_start) DO UPDATE SET + pool_id = EXCLUDED.pool_id, + open = EXCLUDED.open, + high = EXCLUDED.high, + low = EXCLUDED.low, + close = EXCLUDED.close, + volume = EXCLUDED.volume, + volume_quote = EXCLUDED.volume_quote, + volume_usd = EXCLUDED.volume_usd, + trade_count = EXCLUDED.trade_count, + source = EXCLUDED.source, + updated_at = EXCLUDED.updated_at, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; + +CREATE OR REPLACE FUNCTION refresh_wallet_history_flat(target_chain_id TEXT DEFAULT NULL, target_wallet_address TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + INSERT INTO wallet_history_flat(chain_id, tx_hash, wallet_address, pair_address, type, height, timestamp, msg_index, event_index, offer_asset, ask_asset, amount_usd, fee_usd, success, refreshed_at) + SELECT chain_id, tx_hash, wallet_address, pair_address, type, height, timestamp, msg_index, event_index, offer_asset, ask_asset, amount_usd, fee_usd, success, now() + FROM ( + SELECT chain_id, tx_hash, trader AS wallet_address, pair_address, 'swap'::text AS type, height, block_time AS timestamp, msg_index, event_index, + jsonb_build_object('denom', offer_asset, 'amount', offer_amount::text) AS offer_asset, + jsonb_build_object('denom', ask_asset, 'amount', return_amount::text) AS ask_asset, + NULL::numeric AS amount_usd, NULL::numeric AS fee_usd, true AS success + FROM swaps WHERE trader IS NOT NULL + UNION ALL + SELECT chain_id, tx_hash, provider AS wallet_address, pair_address, kind::text AS type, height, block_time AS timestamp, msg_index, event_index, + NULL::jsonb AS offer_asset, NULL::jsonb AS ask_asset, NULL::numeric AS amount_usd, NULL::numeric AS fee_usd, true AS success + FROM liquidity_events WHERE provider IS NOT NULL + UNION ALL + SELECT chain_id, tx_hash, user_address AS wallet_address, NULL::text AS pair_address, action AS type, height, block_time AS timestamp, msg_index, event_index, + NULL::jsonb AS offer_asset, + CASE WHEN reward_asset IS NOT NULL OR reward_amount IS NOT NULL THEN jsonb_build_object('denom', reward_asset, 'amount', reward_amount::text) ELSE NULL::jsonb END AS ask_asset, + NULL::numeric AS amount_usd, NULL::numeric AS fee_usd, true AS success + FROM incentive_events WHERE user_address IS NOT NULL + ) events + WHERE (target_chain_id IS NULL OR chain_id = target_chain_id) + AND (target_wallet_address IS NULL OR wallet_address = target_wallet_address) + ON CONFLICT (chain_id, wallet_address, tx_hash, type, msg_index, event_index) DO UPDATE SET + pair_address = EXCLUDED.pair_address, + height = EXCLUDED.height, + timestamp = EXCLUDED.timestamp, + offer_asset = EXCLUDED.offer_asset, + ask_asset = EXCLUDED.ask_asset, + amount_usd = EXCLUDED.amount_usd, + fee_usd = EXCLUDED.fee_usd, + success = EXCLUDED.success, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; + +CREATE OR REPLACE FUNCTION refresh_wallet_position_latest(target_chain_id TEXT DEFAULT NULL, target_wallet_address TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + INSERT INTO wallet_position_latest(chain_id, wallet_address, pool_id, pair_address, lp_token_address, lp_balance, bonded_balance, last_height, updated_at, refreshed_at) + SELECT chain_id, owner_address, pool_id, pair_address, lp_token_address, lp_balance, bonded_balance, last_height, updated_at, now() + FROM positions + WHERE (target_chain_id IS NULL OR chain_id = target_chain_id) + AND (target_wallet_address IS NULL OR owner_address = target_wallet_address) + ON CONFLICT (chain_id, wallet_address, pair_address) DO UPDATE SET + pool_id = EXCLUDED.pool_id, + lp_token_address = EXCLUDED.lp_token_address, + lp_balance = EXCLUDED.lp_balance, + bonded_balance = EXCLUDED.bonded_balance, + last_height = EXCLUDED.last_height, + updated_at = EXCLUDED.updated_at, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; + +CREATE OR REPLACE FUNCTION refresh_protocol_stats_latest(target_chain_id TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + WITH stats AS ( + SELECT lps.chain_id, + count(*)::int AS pool_count, + count(DISTINCT ie.lp_token_address)::int AS incentivized_pools, + max(COALESCE(lps.state_updated_at, lps.pool_updated_at, lps.refreshed_at)) AS updated_at, + sum(lps.tvl_usd) FILTER (WHERE lps.tvl_usd IS NOT NULL) AS tvl_usd, + sum(lps.tvl_juno) FILTER (WHERE lps.tvl_juno IS NOT NULL) AS tvl_juno, + sum(lps.volume_24h_usd) FILTER (WHERE lps.volume_24h_usd IS NOT NULL) AS volume_24h_usd, + sum(lps.volume_24h_juno) FILTER (WHERE lps.volume_24h_juno IS NOT NULL) AS volume_24h_juno, + sum(lps.volume_7d_usd) FILTER (WHERE lps.volume_7d_usd IS NOT NULL) AS volume_7d_usd, + sum(lps.volume_7d_juno) FILTER (WHERE lps.volume_7d_juno IS NOT NULL) AS volume_7d_juno, + sum(lps.fees_24h_usd) FILTER (WHERE lps.fees_24h_usd IS NOT NULL) AS fees_24h_usd, + sum(lps.fees_24h_juno) FILTER (WHERE lps.fees_24h_juno IS NOT NULL) AS fees_24h_juno + FROM latest_pool_state lps + LEFT JOIN (SELECT DISTINCT chain_id, lp_token_address FROM incentive_events WHERE lp_token_address IS NOT NULL) ie + ON ie.chain_id = lps.chain_id AND ie.lp_token_address = lps.liquidity_token_address + WHERE target_chain_id IS NULL OR lps.chain_id = target_chain_id + GROUP BY lps.chain_id + ) + INSERT INTO protocol_stats_latest(chain_id, pool_count, incentivized_pools, tvl_usd, tvl_juno, volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, updated_at, refreshed_at) + SELECT chain_id, pool_count, incentivized_pools, tvl_usd, tvl_juno, volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, COALESCE(updated_at, now()), now() + FROM stats + ON CONFLICT (chain_id) DO UPDATE SET + pool_count = EXCLUDED.pool_count, + incentivized_pools = EXCLUDED.incentivized_pools, + tvl_usd = EXCLUDED.tvl_usd, + tvl_juno = EXCLUDED.tvl_juno, + volume_24h_usd = EXCLUDED.volume_24h_usd, + volume_24h_juno = EXCLUDED.volume_24h_juno, + volume_7d_usd = EXCLUDED.volume_7d_usd, + volume_7d_juno = EXCLUDED.volume_7d_juno, + fees_24h_usd = EXCLUDED.fees_24h_usd, + fees_24h_juno = EXCLUDED.fees_24h_juno, + updated_at = EXCLUDED.updated_at, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; + +CREATE OR REPLACE FUNCTION refresh_api_read_models(target_chain_id TEXT DEFAULT NULL) +RETURNS TABLE(model TEXT, rows_affected INTEGER) +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN QUERY SELECT 'latest_pool_state'::text, refresh_latest_pool_state(target_chain_id); + RETURN QUERY SELECT 'pool_volume_windows'::text, refresh_pool_volume_windows(target_chain_id); + RETURN QUERY SELECT 'pool_candle_buckets'::text, refresh_pool_candle_buckets(target_chain_id, NULL); + RETURN QUERY SELECT 'wallet_history_flat'::text, refresh_wallet_history_flat(target_chain_id, NULL); + RETURN QUERY SELECT 'wallet_position_latest'::text, refresh_wallet_position_latest(target_chain_id, NULL); + RETURN QUERY SELECT 'protocol_stats_latest'::text, refresh_protocol_stats_latest(target_chain_id); +END; +$$; diff --git a/indexer/migrations/009_juno_stats_derivation.sql b/indexer/migrations/009_juno_stats_derivation.sql new file mode 100644 index 000000000..eecd5854f --- /dev/null +++ b/indexer/migrations/009_juno_stats_derivation.sql @@ -0,0 +1,89 @@ +CREATE OR REPLACE FUNCTION refresh_latest_pool_state(target_chain_id TEXT DEFAULT NULL) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE affected INTEGER; +BEGIN + WITH latest_snapshot AS ( + SELECT DISTINCT ON (p.chain_id, p.pair_address) + p.chain_id, p.id AS pool_id, p.pair_address, p.liquidity_token_address, p.pool_type, + p.asset_infos, p.created_height, p.created_tx_hash, p.first_seen_at, p.updated_at AS pool_updated_at, + s.height AS state_height, s.block_time AS state_block_time, COALESCE(s.reserves, '[]'::jsonb) AS reserves, + s.total_share, s.tvl_usd, COALESCE(s.tvl_juno, reserve_values.tvl_juno) AS tvl_juno, + s.volume_24h_usd, COALESCE(s.volume_24h_juno, swap_values.volume_24h_juno) AS volume_24h_juno, + s.volume_7d_usd, COALESCE(s.volume_7d_juno, swap_values.volume_7d_juno) AS volume_7d_juno, + s.fees_24h_usd, COALESCE(s.fees_24h_juno, swap_values.fees_24h_juno) AS fees_24h_juno, + s.source AS snapshot_source, s.created_at AS state_updated_at + FROM pools p + LEFT JOIN pool_state_snapshots s ON s.pool_id = p.id + LEFT JOIN LATERAL ( + SELECT sum((reserve->>'amount')::numeric) / 1000000 AS tvl_juno + FROM jsonb_array_elements(COALESCE(s.reserves, '[]'::jsonb)) reserve + WHERE reserve->>'denom' = 'ujuno' + AND reserve->>'amount' ~ '^[0-9]+(\.[0-9]+)?$' + ) reserve_values ON true + LEFT JOIN LATERAL ( + SELECT + sum(CASE + WHEN sw.block_time >= now() - interval '24 hours' AND sw.offer_asset = 'ujuno' THEN sw.offer_amount + WHEN sw.block_time >= now() - interval '24 hours' AND sw.ask_asset = 'ujuno' THEN sw.return_amount + ELSE NULL + END) / 1000000 AS volume_24h_juno, + sum(CASE + WHEN sw.offer_asset = 'ujuno' THEN sw.offer_amount + WHEN sw.ask_asset = 'ujuno' THEN sw.return_amount + ELSE NULL + END) / 1000000 AS volume_7d_juno, + sum(CASE + WHEN sw.block_time >= now() - interval '24 hours' AND sw.ask_asset = 'ujuno' THEN sw.commission_amount + ELSE NULL + END) / 1000000 AS fees_24h_juno + FROM swaps sw + WHERE sw.chain_id = p.chain_id + AND sw.pair_address = p.pair_address + AND sw.block_time >= now() - interval '7 days' + ) swap_values ON true + WHERE target_chain_id IS NULL OR p.chain_id = target_chain_id + ORDER BY p.chain_id, p.pair_address, s.height DESC NULLS LAST, + CASE s.source WHEN 'lcd' THEN 0 WHEN 'event' THEN 1 ELSE 2 END, + s.created_at DESC + ) + INSERT INTO latest_pool_state( + chain_id, pool_id, pair_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, + first_seen_at, pool_updated_at, state_height, state_block_time, reserves, total_share, tvl_usd, tvl_juno, + volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, + snapshot_source, state_updated_at, refreshed_at + ) + SELECT chain_id, pool_id, pair_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, + first_seen_at, pool_updated_at, state_height, state_block_time, reserves, total_share, tvl_usd, tvl_juno, + volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, + snapshot_source, state_updated_at, now() + FROM latest_snapshot + ON CONFLICT (chain_id, pair_address) DO UPDATE SET + pool_id = EXCLUDED.pool_id, + liquidity_token_address = EXCLUDED.liquidity_token_address, + pool_type = EXCLUDED.pool_type, + asset_infos = EXCLUDED.asset_infos, + created_height = EXCLUDED.created_height, + created_tx_hash = EXCLUDED.created_tx_hash, + first_seen_at = EXCLUDED.first_seen_at, + pool_updated_at = EXCLUDED.pool_updated_at, + state_height = EXCLUDED.state_height, + state_block_time = EXCLUDED.state_block_time, + reserves = EXCLUDED.reserves, + total_share = EXCLUDED.total_share, + tvl_usd = EXCLUDED.tvl_usd, + tvl_juno = EXCLUDED.tvl_juno, + volume_24h_usd = EXCLUDED.volume_24h_usd, + volume_24h_juno = EXCLUDED.volume_24h_juno, + volume_7d_usd = EXCLUDED.volume_7d_usd, + volume_7d_juno = EXCLUDED.volume_7d_juno, + fees_24h_usd = EXCLUDED.fees_24h_usd, + fees_24h_juno = EXCLUDED.fees_24h_juno, + snapshot_source = EXCLUDED.snapshot_source, + state_updated_at = EXCLUDED.state_updated_at, + refreshed_at = now(); + GET DIAGNOSTICS affected = ROW_COUNT; + RETURN affected; +END; +$$; diff --git a/indexer/package-lock.json b/indexer/package-lock.json new file mode 100644 index 000000000..6f75cf33b --- /dev/null +++ b/indexer/package-lock.json @@ -0,0 +1,1804 @@ +{ + "name": "@astroport-juno/indexer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@astroport-juno/indexer", + "version": "0.1.0", + "dependencies": { + "pg": "^8.13.1" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/pg": "^8.11.10", + "tsx": "^4.19.2", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz", + "integrity": "sha512-F7JnSfPl5ASt6LqwWyUQ3T8BwN3q0eQEbFMYa2iRWaVQmmudo0d7fRmwM4O002gsvW1bs0yBYioutsAjqLJMvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/indexer/package.json b/indexer/package.json new file mode 100644 index 000000000..e2046cd08 --- /dev/null +++ b/indexer/package.json @@ -0,0 +1,33 @@ +{ + "name": "@astroport-juno/indexer", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "Juno Astroport event indexer foundation for Postgres-backed pool metrics.", + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "start": "node dist/src/index.js", + "dev": "tsx src/index.ts", + "migrate": "tsx src/migrate.ts", + "backfill:candles": "tsx src/backfill-candles.ts", + "backfill:range": "tsx src/backfill-range.ts", + "seed:asset-metadata": "tsx src/seed-asset-metadata.ts", + "worker:snapshots": "tsx src/snapshot-worker.ts", + "worker:candles": "tsx src/candle-worker.ts", + "refresh:read-models": "node dist/src/refresh-read-models.js", + "refresh:read-models:dev": "tsx src/refresh-read-models.ts", + "benchmark:range": "tsx src/benchmark-range.ts" + }, + "dependencies": { + "pg": "^8.13.1" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/pg": "^8.11.10", + "tsx": "^4.19.2", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/indexer/src/api-store.ts b/indexer/src/api-store.ts new file mode 100644 index 000000000..207e99955 --- /dev/null +++ b/indexer/src/api-store.ts @@ -0,0 +1,445 @@ +import type { PgPool } from "./db.js"; +import { JunoRpcClient } from "./rpc.js"; +import type { IndexerApiStore, PaginationQuery } from "./api.js"; + +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; +const MAX_CANDLE_LIMIT = 500; +const CANDLE_INTERVALS = new Set(["5m", "1h", "1d"]); + +type Queryable = Pick; +type StoreOptions = { rpcUrl?: string; expectedMigrationCount?: number; expectedMigrationVersions?: string[]; confirmationDepth?: number }; + +function limit(query: PaginationQuery, max = MAX_LIMIT): number { + const parsed = Number.parseInt(query.limit ?? String(DEFAULT_LIMIT), 10); + if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LIMIT; + return Math.min(parsed, max); +} + +function offset(query: PaginationQuery): number { + const parsed = Number.parseInt(query.cursor ?? "0", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; +} + +function page(rows: T[], query: PaginationQuery, max = MAX_LIMIT) { + const safeLimit = limit(query, max); + const start = offset(query); + return { data: rows, pagination: { limit: safeLimit, nextCursor: rows.length === safeLimit ? String(start + safeLimit) : null } }; +} + +function toNumber(value: unknown): number | null { + if (value === null || value === undefined) return null; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function iso(value: unknown): string | null { + if (!value) return null; + return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString(); +} + +function normalizeAssetInfo(value: unknown): string { + if (typeof value === "string") return value; + if (value && typeof value === "object") { + const obj = value as Record; + if (typeof obj.native_token === "object" && obj.native_token) return String((obj.native_token as Record).denom ?? ""); + if (typeof obj.token === "object" && obj.token) return String((obj.token as Record).contract_addr ?? ""); + } + return String(value ?? ""); +} + +function hasValue(value: unknown): boolean { + return value !== null && value !== undefined; +} + +function jsonArray(value: unknown): unknown[] { + if (Array.isArray(value)) return value; + if (typeof value !== "string") return []; + try { + const parsed = JSON.parse(value) as unknown; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function reserveAmountFor(asset: string, reserves: unknown[]): string | null { + for (const reserve of reserves) { + if (!reserve || typeof reserve !== "object") continue; + const row = reserve as Record; + const denom = normalizeAssetInfo(row.denom ?? row.asset ?? row.info ?? row.asset_info); + if (denom === asset && hasValue(row.amount)) return String(row.amount); + } + return null; +} + +function baseAmount(value: unknown): bigint { + const raw = String(value ?? "0"); + return /^\d+$/.test(raw) ? BigInt(raw) : 0n; +} + +function decimalRatio(numerator: bigint, denominator: bigint): number { + if (denominator <= 0n) return 0; + const scaled = (numerator * 1_000_000_000_000n) / denominator; + return Number(scaled) / 1_000_000_000_000; +} + +function prorateBaseAmount(amount: unknown, numerator: bigint, denominator: bigint): string { + if (denominator <= 0n) return "0"; + return ((baseAmount(amount) * numerator) / denominator).toString(); +} + +function normalizePool(row: Record) { + const assetInfos = Array.isArray(row.asset_infos) ? row.asset_infos : []; + const reserves = jsonArray(row.reserves); + const assets = assetInfos.map((asset) => { + const denom = normalizeAssetInfo(asset); + return { denom, reserve: reserveAmountFor(denom, reserves), valueUsd: null, valueJuno: null, priceUsd: null, priceJuno: null, priceStatus: "missing" }; + }); + const updatedAt = iso(row.updated_at ?? row.state_updated_at) ?? new Date(0).toISOString(); + return { + id: String(row.id ?? row.pool_id ?? row.pair_address), + pair: String(row.pair_address), + pairAddress: String(row.pair_address), + lpToken: row.liquidity_token_address ? String(row.liquidity_token_address) : null, + poolType: row.pool_type ? String(row.pool_type) : null, + assets, + totalShare: row.total_share ? String(row.total_share) : null, + tvlUsd: toNumber(row.tvl_usd), + tvlJuno: toNumber(row.tvl_juno), + volume24hUsd: toNumber(row.volume_24h_usd), + volume24hJuno: toNumber(row.volume_24h_juno), + volume7dUsd: toNumber(row.volume_7d_usd), + volume7dJuno: toNumber(row.volume_7d_juno), + fees24hUsd: toNumber(row.fees_24h_usd), + fees24hJuno: toNumber(row.fees_24h_juno), + feeBps: toNumber(row.fee_bps), + feeApr: toNumber(row.fee_apr) ?? 0, + incentivesApr: toNumber(row.incentives_apr) ?? 0, + totalApr: toNumber(row.total_apr) ?? 0, + incentivized: Boolean(row.incentivized), + updatedAt, + dataSource: "indexer", + isMock: false, + }; +} + +function normalizePrice(row: Record | undefined, asset: string) { + if (!row) return { asset, priceUsd: null, priceJuno: null, source: null, status: "missing", stale: false, observedAt: null, ageMs: null, isMock: false }; + const observedAt = iso(row.observed_at); + const ageMs = observedAt ? Date.now() - new Date(observedAt).getTime() : null; + const status = String(row.status ?? (row.price_usd || row.price_juno ? "fresh" : "missing")); + return { asset: String(row.asset ?? asset), priceUsd: toNumber(row.price_usd), priceJuno: toNumber(row.price_juno), source: row.source ? String(row.source) : null, status, stale: status === "stale", observedAt, ageMs, isMock: false }; +} + +export class PostgresApiStore implements IndexerApiStore { + private readonly rpc?: JunoRpcClient; + private readonly expectedMigrationCount?: number; + private readonly expectedMigrationVersions?: string[]; + private readonly confirmationDepth: number; + + constructor(private readonly db: Queryable, private readonly chainId: string, private readonly cursorId = "astroport-juno-v1", options: StoreOptions = {}) { + this.rpc = options.rpcUrl ? new JunoRpcClient(options.rpcUrl) : undefined; + this.expectedMigrationVersions = options.expectedMigrationVersions; + this.expectedMigrationCount = options.expectedMigrationCount ?? options.expectedMigrationVersions?.length; + this.confirmationDepth = Math.max(0, options.confirmationDepth ?? 0); + } + + private async chainHead(): Promise<{ height: number; hash: string } | null> { + if (!this.rpc) return null; + try { + return await this.rpc.head(); + } catch { + return null; + } + } + + private healthFrom(cursorRow: Record | undefined, head: { height: number; hash: string } | null) { + const cursorHeight = toNumber(cursorRow?.last_height); + const cursorUpdatedAt = iso(cursorRow?.updated_at); + const cursorAgeMs = cursorUpdatedAt ? Math.max(0, Date.now() - new Date(cursorUpdatedAt).getTime()) : null; + const confirmedTargetHeight = head ? Math.max(0, head.height - this.confirmationDepth) : null; + return { + status: "ok", + service: "astroport-juno-indexer", + chainId: this.chainId, + confirmationDepth: this.confirmationDepth, + cursorHeight, + cursorBlockHash: cursorRow?.last_block_hash ? String(cursorRow.last_block_hash) : null, + cursorUpdatedAt, + cursorAgeMs, + headHeight: head?.height ?? null, + confirmedTargetHeight, + lag: head && cursorHeight !== null ? Math.max(0, head.height - cursorHeight) : null, + confirmedLag: confirmedTargetHeight !== null && cursorHeight !== null ? Math.max(0, confirmedTargetHeight - cursorHeight) : null, + rpcConfigured: Boolean(this.rpc), + rpcReachable: head !== null, + dataSource: "indexer", + isMock: false, + }; + } + + private readyFrom(appliedVersions: string[], head: { height: number; hash: string } | null) { + const migrationsApplied = appliedVersions.length; + const missingMigrations = this.expectedMigrationVersions?.filter((version) => !appliedVersions.includes(version)) ?? []; + const migrationsCurrent = this.expectedMigrationVersions + ? missingMigrations.length === 0 + : this.expectedMigrationCount === undefined || migrationsApplied >= this.expectedMigrationCount; + const rpcRequired = Boolean(this.rpc); + const rpcOk = !rpcRequired || head !== null; + return { + status: migrationsCurrent && rpcOk ? "ready" : "not_ready", + checks: { database: true, migrations: migrationsCurrent, rpc: rpcOk }, + database: "ok", + migrationsApplied, + expectedMigrations: this.expectedMigrationCount ?? null, + missingMigrations, + rpcConfigured: rpcRequired, + rpcReachable: head !== null, + headHeight: head?.height ?? null, + dataSource: "indexer", + isMock: false, + }; + } + + async health() { + const [cursor, head] = await Promise.all([ + this.db.query(`SELECT last_height, last_block_hash, updated_at FROM indexer_cursors WHERE id = $1`, [this.cursorId]), + this.chainHead(), + ]); + return this.healthFrom(cursor.rows[0], head); + } + + async ready() { + await this.db.query("SELECT 1"); + const [migrations, head] = await Promise.all([ + this.db.query<{ version: string }>(`SELECT version FROM schema_migrations ORDER BY version`), + this.chainHead(), + ]); + return this.readyFrom(migrations.rows.map((row) => row.version), head); + } + + async opsStatus() { + const [cursor, migrations, head] = await Promise.all([ + this.db.query(`SELECT last_height, last_block_hash, updated_at FROM indexer_cursors WHERE id = $1`, [this.cursorId]), + (async () => { + await this.db.query("SELECT 1"); + return this.db.query<{ version: string }>(`SELECT version FROM schema_migrations ORDER BY version`); + })(), + this.chainHead(), + ]); + return { health: this.healthFrom(cursor.rows[0], head), ready: this.readyFrom(migrations.rows.map((row) => row.version), head) }; + } + + async stats() { + const result = await this.db.query( + `SELECT pool_count, incentivized_pools, updated_at, tvl_usd, tvl_juno, + volume_24h_usd, volume_24h_juno, volume_7d_usd, volume_7d_juno, + fees_24h_usd, fees_24h_juno + FROM protocol_stats_latest + WHERE chain_id = $1`, + [this.chainId], + ); + const row = result.rows[0] ?? {}; + return { + poolCount: Number(row.pool_count ?? 0), + tvlUsd: hasValue(row.tvl_usd) ? toNumber(row.tvl_usd) : null, + tvlJuno: hasValue(row.tvl_juno) ? toNumber(row.tvl_juno) : null, + volume24hUsd: hasValue(row.volume_24h_usd) ? toNumber(row.volume_24h_usd) : null, + volume24hJuno: hasValue(row.volume_24h_juno) ? toNumber(row.volume_24h_juno) : null, + volume7dUsd: hasValue(row.volume_7d_usd) ? toNumber(row.volume_7d_usd) : null, + volume7dJuno: hasValue(row.volume_7d_juno) ? toNumber(row.volume_7d_juno) : null, + fees24hUsd: hasValue(row.fees_24h_usd) ? toNumber(row.fees_24h_usd) : null, + fees24hJuno: hasValue(row.fees_24h_juno) ? toNumber(row.fees_24h_juno) : null, + incentivizedPools: Number(row.incentivized_pools ?? 0), + updatedAt: iso(row.updated_at) ?? new Date(0).toISOString(), + dataSource: "indexer", + isMock: false, + }; + } + + async prices(assets: string[]) { + const result = await this.db.query( + `SELECT DISTINCT ON (asset) asset, price_usd, price_juno, source, status, observed_at + FROM token_prices WHERE chain_id = $1 AND asset = ANY($2::text[]) + ORDER BY asset, observed_at DESC`, + [this.chainId, assets], + ); + const byAsset = new Map(result.rows.map((row: Record) => [String(row.asset), row])); + return assets.map((asset) => normalizePrice(byAsset.get(asset), asset)); + } + + async pools(query: PaginationQuery) { + const safeLimit = limit(query); + const result = await this.db.query( + `SELECT pool_id AS id, chain_id, pair_address, liquidity_token_address, pool_type, + asset_infos, created_height, created_tx_hash, first_seen_at, pool_updated_at AS updated_at, + reserves, total_share, tvl_usd, tvl_juno, volume_24h_usd, volume_24h_juno, + volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, state_updated_at + FROM latest_pool_state + WHERE chain_id = $1 AND ($2::text IS NULL OR pair_address = $2) + ORDER BY COALESCE(tvl_usd, 0) DESC, created_height DESC NULLS LAST + LIMIT $3 OFFSET $4`, + [this.chainId, query.pair ?? null, safeLimit, offset(query)], + ); + return page(result.rows.map(normalizePool), query); + } + + async pool(id: string) { + const readModel = await this.db.query( + `SELECT pool_id AS id, chain_id, pair_address, liquidity_token_address, pool_type, + asset_infos, created_height, created_tx_hash, first_seen_at, pool_updated_at AS updated_at, + reserves, total_share, tvl_usd, tvl_juno, volume_24h_usd, volume_24h_juno, + volume_7d_usd, volume_7d_juno, fees_24h_usd, fees_24h_juno, state_updated_at + FROM latest_pool_state + WHERE chain_id = $1 AND (pool_id::text = $2 OR pair_address = $2) LIMIT 1`, + [this.chainId, id], + ); + if (readModel.rows[0]) return normalizePool(readModel.rows[0]); + + const result = await this.db.query( + `SELECT p.*, lps.reserves, lps.total_share, lps.tvl_usd, lps.tvl_juno, lps.volume_24h_usd, lps.volume_24h_juno, + lps.volume_7d_usd, lps.volume_7d_juno, lps.fees_24h_usd, lps.fees_24h_juno, + lps.state_updated_at + FROM pools p + LEFT JOIN latest_pool_states lps ON lps.chain_id = p.chain_id AND lps.pair_address = p.pair_address + WHERE p.chain_id = $1 AND (p.id::text = $2 OR p.pair_address = $2) LIMIT 1`, + [this.chainId, id], + ); + return result.rows[0] ? normalizePool(result.rows[0]) : null; + } + + async candles(id: string, query: PaginationQuery) { + const interval = query.interval ?? "1h"; + if (!CANDLE_INTERVALS.has(interval)) throw new RangeError(`unsupported interval: ${interval}`); + const pool = await this.pool(id); + if (!pool) return null; + const pairAddress = String(pool.pairAddress); + const safeLimit = limit(query, MAX_CANDLE_LIMIT); + const values = [this.chainId, pairAddress, interval, query.baseAsset ?? null, query.quoteAsset ?? null, query.from ?? null, query.to ?? null, safeLimit, offset(query)]; + let result = await this.db.query( + `SELECT pair_address, pool_id, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, trade_count + FROM pool_candle_buckets + WHERE chain_id = $1 AND pair_address = $2 AND interval = $3 + AND ($4::text IS NULL OR asset = $4) + AND ($5::text IS NULL OR quote_asset = $5) + AND ($6::timestamptz IS NULL OR bucket_start >= $6) + AND ($7::timestamptz IS NULL OR bucket_start <= $7) + ORDER BY bucket_start DESC LIMIT $8 OFFSET $9`, + values, + ); + const filterFallback = result.rows.length === 0 && Boolean(query.baseAsset || query.quoteAsset); + if (filterFallback) { + result = await this.db.query( + `SELECT pair_address, pool_id, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, trade_count + FROM pool_candle_buckets + WHERE chain_id = $1 AND pair_address = $2 AND interval = $3 + AND ($4::timestamptz IS NULL OR bucket_start >= $4) + AND ($5::timestamptz IS NULL OR bucket_start <= $5) + ORDER BY bucket_start DESC LIMIT $6 OFFSET $7`, + [this.chainId, pairAddress, interval, query.from ?? null, query.to ?? null, safeLimit, offset(query)], + ); + } + const data = result.rows.map((row: Record) => ({ poolId: row.pool_id ? String(row.pool_id) : String(pool.id), pairAddress: String(row.pair_address), baseAsset: String(row.asset), quoteAsset: String(row.quote_asset), interval: String(row.interval), bucketStart: iso(row.bucket_start), open: toNumber(row.open), high: toNumber(row.high), low: toNumber(row.low), close: toNumber(row.close), volume: toNumber(row.volume), volumeQuote: toNumber(row.volume_quote), tradeCount: Number(row.trade_count ?? 0), dataSource: "indexer", isMock: false })); + return { ...page(data, query, MAX_CANDLE_LIMIT), meta: { poolId: String(pool.id), pairAddress, interval, baseAsset: filterFallback ? null : query.baseAsset ?? null, quoteAsset: filterFallback ? null : query.quoteAsset ?? null, requestedBaseAsset: query.baseAsset ?? null, requestedQuoteAsset: query.quoteAsset ?? null, filterFallback, from: query.from ?? null, to: query.to ?? null, dataSource: "indexer", isMock: false } }; + } + + async poolPositions(id: string, query: PaginationQuery) { + const result = await this.db.query( + `SELECT w.wallet_address AS owner_address, w.pool_id, w.pair_address, w.lp_token_address, + w.lp_balance, w.bonded_balance, w.updated_at, + lps.asset_infos, lps.reserves, lps.total_share, lps.tvl_usd, lps.tvl_juno + FROM wallet_position_latest w + LEFT JOIN latest_pool_state lps ON lps.chain_id = w.chain_id AND lps.pair_address = w.pair_address + WHERE w.chain_id = $1 AND (w.pool_id::text = $2 OR w.pair_address = $2) + ORDER BY w.updated_at DESC LIMIT $3 OFFSET $4`, + [this.chainId, id, limit(query), offset(query)], + ); + return page(result.rows.map(normalizePosition), query); + } + + async poolHistory(id: string, query: PaginationQuery) { + const pool = await this.pool(id); + if (!pool) return page([], query); + const pairAddress = String(pool.pairAddress); + const result = await this.db.query( + `SELECT tx_hash, wallet_address, pair_address, type, height, timestamp, + offer_asset, ask_asset, amount_usd, fee_usd, success + FROM wallet_history_flat + WHERE chain_id = $1 AND pair_address = $2 + ORDER BY height DESC, timestamp DESC LIMIT $3 OFFSET $4`, + [this.chainId, pairAddress, limit(query), offset(query)], + ); + return page(result.rows.map(normalizeTx), query); + } + + async walletPositions(addr: string, query: PaginationQuery) { + const result = await this.db.query( + `SELECT w.wallet_address AS owner_address, w.pool_id, w.pair_address, w.lp_token_address, + w.lp_balance, w.bonded_balance, w.updated_at, + lps.asset_infos, lps.reserves, lps.total_share, lps.tvl_usd, lps.tvl_juno + FROM wallet_position_latest w + LEFT JOIN latest_pool_state lps ON lps.chain_id = w.chain_id AND lps.pair_address = w.pair_address + WHERE w.chain_id = $1 AND w.wallet_address = $2 + ORDER BY w.updated_at DESC LIMIT $3 OFFSET $4`, + [this.chainId, addr, limit(query), offset(query)], + ); + return page(result.rows.map(normalizePosition), query); + } + + async walletHistory(addr: string, query: PaginationQuery) { + const result = await this.db.query( + `SELECT tx_hash, wallet_address, pair_address, type, height, timestamp, + offer_asset, ask_asset, amount_usd, fee_usd, success + FROM wallet_history_flat + WHERE chain_id = $1 AND wallet_address = $2 + ORDER BY height DESC, timestamp DESC LIMIT $3 OFFSET $4`, + [this.chainId, addr, limit(query), offset(query)], + ); + return page(result.rows.map(normalizeTx), query); + } +} + +function normalizePosition(row: Record) { + const lpBalance = String(row.lp_balance ?? "0"); + const bondedBalance = String(row.bonded_balance ?? "0"); + const totalPositionLp = baseAmount(lpBalance) + baseAmount(bondedBalance); + const totalShare = baseAmount(row.total_share); + const share = decimalRatio(totalPositionLp, totalShare); + const assetInfos = Array.isArray(row.asset_infos) ? row.asset_infos : []; + const reserves = jsonArray(row.reserves); + const assets = assetInfos.map((asset) => { + const denom = normalizeAssetInfo(asset); + return { + denom, + reserve: reserveAmountFor(denom, reserves), + amount: prorateBaseAmount(reserveAmountFor(denom, reserves), totalPositionLp, totalShare), + valueUsd: null, + valueJuno: null, + priceUsd: null, + priceJuno: null, + priceStatus: "missing", + }; + }); + const tvlUsd = toNumber(row.tvl_usd); + const tvlJuno = toNumber(row.tvl_juno); + return { + walletAddress: String(row.owner_address), + poolId: String(row.pool_id ?? row.pair_address), + pairAddress: String(row.pair_address), + lpToken: row.lp_token_address ? String(row.lp_token_address) : null, + lpBalance, + bondedBalance, + shareBps: Math.round(share * 10_000), + valueUsd: tvlUsd === null || share <= 0 ? null : tvlUsd * share, + valueJuno: tvlJuno === null || share <= 0 ? null : tvlJuno * share, + assets, + updatedAt: iso(row.updated_at) ?? new Date(0).toISOString(), + dataSource: "indexer", + isMock: false, + }; +} + +function normalizeTx(row: Record) { + return { txHash: String(row.tx_hash), walletAddress: row.wallet_address ? String(row.wallet_address) : null, poolId: row.pair_address ? String(row.pair_address) : null, pairAddress: row.pair_address ? String(row.pair_address) : null, type: String(row.type), height: Number(row.height), timestamp: iso(row.timestamp) ?? new Date(0).toISOString(), offerAsset: row.offer_asset ?? null, askAsset: row.ask_asset ?? null, amountUsd: toNumber(row.amount_usd), feeUsd: toNumber(row.fee_usd), success: Boolean(row.success), dataSource: "indexer", isMock: false }; +} diff --git a/indexer/src/api.ts b/indexer/src/api.ts new file mode 100644 index 000000000..483eee3d4 --- /dev/null +++ b/indexer/src/api.ts @@ -0,0 +1,205 @@ +import http from "node:http"; +import { URL } from "node:url"; +import type { IndexerMetrics } from "./metrics.js"; +import { openApiDocument } from "./openapi.js"; + +export type PaginationQuery = { + limit?: string; + cursor?: string; + pair?: string; + interval?: string; + from?: string; + to?: string; + baseAsset?: string; + quoteAsset?: string; +}; + +export type IndexerApiStore = { + health(): Promise>; + ready(): Promise>; + opsStatus(): Promise<{ health: Record; ready: Record }>; + stats(): Promise>; + prices(assets: string[]): Promise[]>; + pools(query: PaginationQuery): Promise>; + pool(id: string): Promise | null>; + candles(id: string, query: PaginationQuery): Promise | null>; + poolPositions(id: string, query: PaginationQuery): Promise>; + poolHistory(id: string, query: PaginationQuery): Promise>; + walletPositions(addr: string, query: PaginationQuery): Promise>; + walletHistory(addr: string, query: PaginationQuery): Promise>; +}; + +function baseHeaders(extraHeaders: Record = {}) { + return { + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET, OPTIONS", + "access-control-allow-headers": "content-type, authorization", + ...extraHeaders, + }; +} + +function jsonResponse(res: http.ServerResponse, status: number, body: unknown, extraHeaders: Record = {}) { + const payload = status === 204 ? "" : JSON.stringify(body); + res.writeHead(status, baseHeaders({ + "content-type": "application/json; charset=utf-8", + "cache-control": status === 200 ? "public, max-age=15, stale-while-revalidate=30" : "no-store", + ...extraHeaders, + })); + res.end(payload); +} + +function textResponse(res: http.ServerResponse, status: number, body: string, extraHeaders: Record = {}) { + res.writeHead(status, baseHeaders({ + "content-type": "text/plain; version=0.0.4; charset=utf-8", + "cache-control": "no-store", + ...extraHeaders, + })); + res.end(body); +} + +function query(searchParams: URLSearchParams): PaginationQuery { + return { + limit: searchParams.get("limit") ?? undefined, + cursor: searchParams.get("cursor") ?? undefined, + pair: searchParams.get("pair") ?? undefined, + interval: searchParams.get("interval") ?? undefined, + from: searchParams.get("from") ?? undefined, + to: searchParams.get("to") ?? undefined, + baseAsset: searchParams.get("baseAsset") ?? searchParams.get("base_asset") ?? undefined, + quoteAsset: searchParams.get("quoteAsset") ?? searchParams.get("quote_asset") ?? undefined, + }; +} + +function assets(searchParams: URLSearchParams, pathAsset?: string): string[] { + const values: string[] = []; + if (pathAsset) values.push(pathAsset); + for (const key of ["asset", "assets", "denom", "denoms"]) { + const value = searchParams.get(key); + if (value) values.push(...value.split(",")); + } + return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean))); +} + +function metricHelp(name: string, help: string, type = "gauge") { + return [`# HELP ${name} ${help}`, `# TYPE ${name} ${type}`]; +} + +function metricValue(value: unknown): number | null { + if (typeof value === "boolean") return value ? 1 : 0; + if (value === null || value === undefined) return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function labelValue(value: unknown): string { + return String(value ?? "unknown").replace(/[\\"\n]/g, "_"); +} + +function metricLine(name: string, value: unknown, labels: Record = {}) { + const number = metricValue(value); + if (number === null) return null; + const labelEntries = Object.entries(labels); + const renderedLabels = labelEntries.length > 0 ? `{${labelEntries.map(([key, label]) => `${key}="${labelValue(label)}"`).join(",")}}` : ""; + return `${name}${renderedLabels} ${number}`; +} + +async function metricsBody(store: IndexerApiStore, metrics?: IndexerMetrics): Promise { + const { health, ready } = await store.opsStatus(); + const labels = { chain_id: health.chainId ?? "unknown" }; + const lines = [ + ...metricHelp("juno_indexer_ready", "Indexer readiness status: 1 when /ready is ready, otherwise 0."), + metricLine("juno_indexer_ready", ready.status === "ready", labels), + ...metricHelp("juno_indexer_rpc_configured", "Whether this API store has an RPC endpoint configured for chain head checks."), + metricLine("juno_indexer_rpc_configured", health.rpcConfigured, labels), + ...metricHelp("juno_indexer_rpc_reachable", "RPC reachability; meaningful when juno_indexer_rpc_configured is 1."), + metricLine("juno_indexer_rpc_reachable", health.rpcReachable, labels), + ...metricHelp("juno_indexer_cursor_height", "Last block height committed to the indexer cursor."), + metricLine("juno_indexer_cursor_height", health.cursorHeight, labels), + ...metricHelp("juno_indexer_head_height", "Latest chain head height observed by the indexer API."), + metricLine("juno_indexer_head_height", health.headHeight, labels), + ...metricHelp("juno_indexer_confirmed_target_height", "Latest chain height considered safe after confirmation depth."), + metricLine("juno_indexer_confirmed_target_height", health.confirmedTargetHeight, labels), + ...metricHelp("juno_indexer_lag_blocks", "Difference between observed chain head and indexer cursor height."), + metricLine("juno_indexer_lag_blocks", health.lag, labels), + ...metricHelp("juno_indexer_confirmed_lag_blocks", "Difference between confirmed target height and indexer cursor height."), + metricLine("juno_indexer_confirmed_lag_blocks", health.confirmedLag, labels), + ...metricHelp("juno_indexer_cursor_age_ms", "Milliseconds since the indexer cursor row was last updated."), + metricLine("juno_indexer_cursor_age_ms", health.cursorAgeMs, labels), + ...metricHelp("juno_indexer_migrations_applied", "Number of schema migrations recorded as applied."), + metricLine("juno_indexer_migrations_applied", ready.migrationsApplied, labels), + ...metricHelp("juno_indexer_expected_migrations", "Expected schema migration count when configured."), + metricLine("juno_indexer_expected_migrations", ready.expectedMigrations, labels), + ]; + if (metrics) { + const snapshot = metrics.snapshot(); + lines.push( + ...metricHelp("juno_indexer_fetch_blocks_total", "Blocks fetched by the in-process indexer fetcher.", "counter"), + metricLine("juno_indexer_fetch_blocks_total", snapshot.fetchBlocksTotal), + ...metricHelp("juno_indexer_fetch_blocks_per_second", "Average block fetch throughput since process start."), + metricLine("juno_indexer_fetch_blocks_per_second", snapshot.fetchBlocksPerSecond), + ...metricHelp("juno_indexer_fetch_rpc_requests_in_flight", "RPC requests currently in flight."), + metricLine("juno_indexer_fetch_rpc_requests_in_flight", snapshot.rpcRequestsInFlight), + ...metricHelp("juno_indexer_fetch_rpc_error_total", "RPC fetch errors by low-cardinality status.", "counter"), + ); + for (const [status, count] of snapshot.rpcErrors) lines.push(metricLine("juno_indexer_fetch_rpc_error_total", count, { status })); + lines.push( + ...metricHelp("juno_indexer_decode_blocks_total", "Blocks decoded by the in-process indexer.", "counter"), + metricLine("juno_indexer_decode_blocks_total", snapshot.decodeBlocksTotal), + ...metricHelp("juno_indexer_writer_blocks_total", "Blocks committed by the indexer writer.", "counter"), + metricLine("juno_indexer_writer_blocks_total", snapshot.writerBlocksTotal), + ...metricHelp("juno_indexer_writer_commit_seconds", "Most recent block writer commit duration in seconds."), + metricLine("juno_indexer_writer_commit_seconds", snapshot.writerCommitSeconds), + ...metricHelp("juno_indexer_writer_events_total", "Events committed by the indexer writer by normalized kind.", "counter"), + ); + for (const [kind, count] of snapshot.writerEvents) lines.push(metricLine("juno_indexer_writer_events_total", count, { kind })); + lines.push( + ...metricHelp("juno_indexer_reorg_halt", "Whether ingestion is halted because of reorg protection."), + metricLine("juno_indexer_reorg_halt", snapshot.reorgHalt), + ); + } + return `${lines.filter((line): line is string => line !== null).join("\n")}\n`; +} + +export function createIndexerApi(store: IndexerApiStore, metrics?: IndexerMetrics): http.Server { + return http.createServer(async (req, res) => { + if (req.method === "OPTIONS") return jsonResponse(res, 204, {}); + if (req.method !== "GET") return jsonResponse(res, 405, { error: "method_not_allowed" }); + const url = new URL(req.url ?? "/", "http://localhost"); + const parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent); + const parsedQuery = query(url.searchParams); + try { + if (url.pathname === "/health") return jsonResponse(res, 200, await store.health(), { "cache-control": "no-store" }); + if (url.pathname === "/ready") { + const body = await store.ready(); + return jsonResponse(res, body.status === "ready" ? 200 : 503, body, { "cache-control": "no-store" }); + } + if (url.pathname === "/metrics") return textResponse(res, 200, await metricsBody(store, metrics)); + if (url.pathname === "/openapi.json") return jsonResponse(res, 200, openApiDocument); + if (url.pathname === "/stats") return jsonResponse(res, 200, await store.stats()); + if (parts[0] === "prices" && parts.length <= 2) { + const ids = assets(url.searchParams, parts[1]); + if (ids.length === 0) return jsonResponse(res, 400, { error: "asset_required" }); + const prices = await store.prices(ids); + return jsonResponse(res, 200, parts[1] ? prices[0] ?? null : { data: prices }); + } + if (parts[0] === "pools" && parts.length === 1) return jsonResponse(res, 200, await store.pools(parsedQuery)); + if (parts[0] === "pools" && parts.length === 2) { + const pool = await store.pool(parts[1]); + return pool ? jsonResponse(res, 200, pool) : jsonResponse(res, 404, { error: "pool_not_found" }); + } + if (parts[0] === "pools" && parts.length === 3 && parts[2] === "candles") { + const page = await store.candles(parts[1], parsedQuery); + return page ? jsonResponse(res, 200, page) : jsonResponse(res, 404, { error: "pool_not_found" }); + } + if (parts[0] === "pools" && parts.length === 3 && parts[2] === "positions") return jsonResponse(res, 200, await store.poolPositions(parts[1], parsedQuery)); + if (parts[0] === "pools" && parts.length === 3 && parts[2] === "history") return jsonResponse(res, 200, await store.poolHistory(parts[1], parsedQuery)); + if (parts[0] === "wallets" && parts.length === 3 && parts[2] === "positions") return jsonResponse(res, 200, await store.walletPositions(parts[1], parsedQuery)); + if (parts[0] === "wallets" && parts.length === 3 && parts[2] === "history") return jsonResponse(res, 200, await store.walletHistory(parts[1], parsedQuery)); + return jsonResponse(res, 404, { error: "not_found" }); + } catch (error) { + if (error instanceof RangeError) return jsonResponse(res, 400, { error: "bad_request", message: error.message }); + console.error("indexer_api_error", error); + return jsonResponse(res, 500, { error: "internal_error" }); + } + }); +} diff --git a/indexer/src/backfill-candles.ts b/indexer/src/backfill-candles.ts new file mode 100644 index 000000000..17d9f52ff --- /dev/null +++ b/indexer/src/backfill-candles.ts @@ -0,0 +1,72 @@ +import { loadConfig } from "./config.js"; +import { backfillTokenCandles, createPool } from "./db.js"; + +const args = new Map(); +for (const arg of process.argv.slice(2)) { + const [key, value = ""] = arg.replace(/^--/, "").split("="); + if (key) args.set(key, value); +} + +const config = loadConfig(); +const pool = createPool(config); +const client = await pool.connect(); +try { + const chainId = args.get("chain-id") ?? config.chainId; + const pairAddress = args.get("pair") || undefined; + const from = args.get("from") || undefined; + const to = args.get("to") || undefined; + const processed = await backfillTokenCandles(client, { + chainId, + pairAddress, + from, + to, + batchSize: args.get("limit") ? Number(args.get("limit")) : undefined, + }); + console.log(`backfilled candle inputs processed=${processed}`); + + const diagnostics = await client.query<{ + swap_count: string; + eligible_swap_count: string; + missing_assets: string[] | null; + token_candle_count: string; + }>( + `WITH selected_swaps AS ( + SELECT offer_asset, ask_asset + FROM swaps + WHERE chain_id = $1 + AND ($2::text IS NULL OR pair_address = $2) + AND ($3::timestamptz IS NULL OR block_time >= $3) + AND ($4::timestamptz IS NULL OR block_time <= $4) + ), + swap_assets AS ( + SELECT offer_asset AS asset FROM selected_swaps WHERE offer_asset IS NOT NULL + UNION + SELECT ask_asset AS asset FROM selected_swaps WHERE ask_asset IS NOT NULL + ), + asset_status AS ( + SELECT a.asset, m.decimals + FROM swap_assets a + LEFT JOIN asset_metadata m ON m.chain_id = $1 AND m.asset = a.asset + ), + eligible_swaps AS ( + SELECT 1 + FROM selected_swaps s + JOIN asset_metadata offer_meta ON offer_meta.chain_id = $1 AND offer_meta.asset = s.offer_asset AND offer_meta.decimals BETWEEN 0 AND 36 + JOIN asset_metadata ask_meta ON ask_meta.chain_id = $1 AND ask_meta.asset = s.ask_asset AND ask_meta.decimals BETWEEN 0 AND 36 + ) + SELECT + (SELECT count(*) FROM selected_swaps)::text AS swap_count, + (SELECT count(*) FROM eligible_swaps)::text AS eligible_swap_count, + (SELECT array_agg(asset ORDER BY asset) FROM asset_status WHERE decimals IS NULL OR decimals < 0 OR decimals > 36) AS missing_assets, + (SELECT count(*) FROM token_candles WHERE chain_id = $1 AND ($2::text IS NULL OR pair_address = $2) AND ($3::timestamptz IS NULL OR bucket_start >= $3) AND ($4::timestamptz IS NULL OR bucket_start <= $4))::text AS token_candle_count`, + [chainId, pairAddress ?? null, from ?? null, to ?? null], + ); + const stats = diagnostics.rows[0]; + if (stats) { + console.log(`candle diagnostics swaps=${stats.swap_count} eligible_swaps=${stats.eligible_swap_count} token_candles=${stats.token_candle_count}`); + if (stats.missing_assets?.length) console.log(`candle diagnostics missing_or_invalid_decimals=${stats.missing_assets.join(",")}`); + } +} finally { + client.release(); + await pool.end(); +} diff --git a/indexer/src/backfill-range.ts b/indexer/src/backfill-range.ts new file mode 100644 index 000000000..ce69577ed --- /dev/null +++ b/indexer/src/backfill-range.ts @@ -0,0 +1,33 @@ +import { parseNonNegativeInteger } from "./ranges.js"; +import { loadConfig } from "./config.js"; +import { createPool, runMigrations } from "./db.js"; +import { Indexer } from "./indexer.js"; + +function intArg(name: string): number | undefined { + const prefix = `--${name}=`; + const arg = process.argv.find((value) => value.startsWith(prefix)); + const raw = arg ? arg.slice(prefix.length) : process.env[name.toUpperCase().replace(/-/g, "_")]; + if (!raw) return undefined; + return parseNonNegativeInteger(raw, name); +} + +const toHeight = intArg("to-height"); +if (toHeight === undefined) throw new Error("missing --to-height= or TO_HEIGHT"); + +const config = loadConfig(); +const pool = createPool(config); +const indexer = new Indexer(config, pool); +let totalProcessed = 0; +try { + const applied = await runMigrations(pool); + if (applied.length > 0) console.log(`migrations applied=${applied.join(",")}`); + for (;;) { + const result = await indexer.runUntilHeight(toHeight); + totalProcessed += result.processed; + console.log(`bounded backfill processed=${result.processed} total=${totalProcessed} head=${result.head} target=${result.target} cursor=${result.cursorHeight} to_height=${toHeight}`); + if (result.done) break; + } +} finally { + await indexer.close(); + await pool.end(); +} diff --git a/indexer/src/benchmark-range.ts b/indexer/src/benchmark-range.ts new file mode 100644 index 000000000..0dccc15f4 --- /dev/null +++ b/indexer/src/benchmark-range.ts @@ -0,0 +1,180 @@ +import { loadConfig } from "./config.js"; +import { createPool, runMigrations, type PgPool } from "./db.js"; +import { Indexer } from "./indexer.js"; +import { parseNonNegativeInteger } from "./ranges.js"; + +type EventCounts = { + poolsCreated: number | null; + swaps: number | null; + liquidityProvides: number | null; + liquidityWithdraws: number | null; + incentives: number | null; +}; + +type BenchmarkSummary = { + blockRange: { from: number; to: number }; + durationMs: number; + durationSeconds: number; + blocksProcessed: number; + blocksPerSecond: number; + cursor: number | null; + head: number | null; + target: number | null; + lag: number | null; + rpcErrorCount: number; + eventCounts: EventCounts; + migrationsApplied: string[]; + error?: string; +}; + +function intArg(name: string): number | undefined { + const prefix = `--${name}=`; + const arg = process.argv.find((value) => value.startsWith(prefix)); + const raw = arg ? arg.slice(prefix.length) : process.env[name.toUpperCase().replace(/-/g, "_")]; + if (!raw) return undefined; + return parseNonNegativeInteger(raw, name); +} + +function isRpcLikeError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return /\b(RPC|LCD|fetch|status|block_results|\/block\?|\/status)\b/i.test(message); +} + +async function setCursor(pool: PgPool, params: { cursorId: string; chainId: string; height: number }): Promise { + await pool.query( + `INSERT INTO indexer_cursors(id, chain_id, last_height, last_block_hash) + VALUES ($1, $2, $3, NULL) + ON CONFLICT (id) DO UPDATE + SET chain_id = EXCLUDED.chain_id, + last_height = EXCLUDED.last_height, + last_block_hash = NULL, + updated_at = now()`, + [params.cursorId, params.chainId, params.height], + ); +} + +async function getCursorHeight(pool: PgPool, cursorId: string): Promise { + const result = await pool.query<{ last_height: string }>( + `SELECT last_height FROM indexer_cursors WHERE id = $1`, + [cursorId], + ); + const raw = result.rows[0]?.last_height; + return raw === undefined ? null : Number(raw); +} + +async function eventCounts(pool: PgPool, chainId: string, fromHeight: number, toHeight: number): Promise { + const [pools, swaps, liquidity, incentives] = await Promise.all([ + pool.query<{ count: string }>( + `SELECT count(*)::text AS count + FROM pools + WHERE chain_id = $1 AND created_height BETWEEN $2 AND $3`, + [chainId, fromHeight, toHeight], + ), + pool.query<{ count: string }>( + `SELECT count(*)::text AS count + FROM swaps + WHERE chain_id = $1 AND height BETWEEN $2 AND $3`, + [chainId, fromHeight, toHeight], + ), + pool.query<{ kind: "provide" | "withdraw"; count: string }>( + `SELECT kind, count(*)::text AS count + FROM liquidity_events + WHERE chain_id = $1 AND height BETWEEN $2 AND $3 + GROUP BY kind`, + [chainId, fromHeight, toHeight], + ), + pool.query<{ count: string }>( + `SELECT count(*)::text AS count + FROM incentive_events + WHERE chain_id = $1 AND height BETWEEN $2 AND $3`, + [chainId, fromHeight, toHeight], + ), + ]); + + const liquidityByKind = new Map(liquidity.rows.map((row) => [row.kind, Number(row.count)])); + return { + poolsCreated: Number(pools.rows[0]?.count ?? 0), + swaps: Number(swaps.rows[0]?.count ?? 0), + liquidityProvides: liquidityByKind.get("provide") ?? 0, + liquidityWithdraws: liquidityByKind.get("withdraw") ?? 0, + incentives: Number(incentives.rows[0]?.count ?? 0), + }; +} + +const fromHeight = intArg("from-height"); +const toHeight = intArg("to-height"); +if (fromHeight === undefined) throw new Error("missing --from-height= or FROM_HEIGHT"); +if (toHeight === undefined) throw new Error("missing --to-height= or TO_HEIGHT"); +if (toHeight < fromHeight) throw new Error("--to-height must be greater than or equal to --from-height"); + +const config = loadConfig(); +const pool = createPool(config); +const indexer = new Indexer({ ...config, dryRun: false, startHeight: fromHeight }, pool); +const start = Date.now(); +let blocksProcessed = 0; +let cursor: number | null = null; +let head: number | null = null; +let target: number | null = null; +let rpcErrorCount = 0; +let migrationsApplied: string[] = []; + +try { + migrationsApplied = await runMigrations(pool); + await setCursor(pool, { cursorId: config.cursorId, chainId: config.chainId, height: Math.max(0, fromHeight - 1) }); + + for (;;) { + try { + const result = await indexer.runUntilHeight(toHeight); + blocksProcessed += result.processed; + cursor = result.cursorHeight; + head = result.head; + target = result.target; + if (result.done) break; + } catch (error) { + if (isRpcLikeError(error)) rpcErrorCount += 1; + throw error; + } + } + + cursor = await getCursorHeight(pool, config.cursorId); + const durationMs = Date.now() - start; + const counts = await eventCounts(pool, config.chainId, fromHeight, toHeight); + const summary: BenchmarkSummary = { + blockRange: { from: fromHeight, to: toHeight }, + durationMs, + durationSeconds: durationMs / 1000, + blocksProcessed, + blocksPerSecond: durationMs > 0 ? blocksProcessed / (durationMs / 1000) : blocksProcessed, + cursor, + head, + target, + lag: target === null || cursor === null ? null : Math.max(0, target - cursor), + rpcErrorCount, + eventCounts: counts, + migrationsApplied, + }; + console.log(JSON.stringify(summary)); +} catch (error) { + const durationMs = Date.now() - start; + cursor = await getCursorHeight(pool, config.cursorId).catch(() => cursor); + const summary: BenchmarkSummary = { + blockRange: { from: fromHeight, to: toHeight }, + durationMs, + durationSeconds: durationMs / 1000, + blocksProcessed, + blocksPerSecond: durationMs > 0 ? blocksProcessed / (durationMs / 1000) : blocksProcessed, + cursor, + head, + target, + lag: target === null || cursor === null ? null : Math.max(0, target - cursor), + rpcErrorCount, + eventCounts: { poolsCreated: null, swaps: null, liquidityProvides: null, liquidityWithdraws: null, incentives: null }, + migrationsApplied, + error: error instanceof Error ? error.message : String(error), + }; + console.log(JSON.stringify(summary)); + process.exitCode = 1; +} finally { + await indexer.close(); + await pool.end(); +} diff --git a/indexer/src/block-fetcher.ts b/indexer/src/block-fetcher.ts new file mode 100644 index 000000000..126381532 --- /dev/null +++ b/indexer/src/block-fetcher.ts @@ -0,0 +1,45 @@ +import type { BlockBundle, JunoRpcClient } from "./rpc.js"; + +export type FetchBlockRangeParams = { + rpc: JunoRpcClient; + from: number; + to: number; + concurrency: number; +}; + +export async function fetchBlockRange({ rpc, from, to, concurrency }: FetchBlockRangeParams): Promise { + if (!Number.isInteger(from) || !Number.isInteger(to)) throw new Error("block range bounds must be integer heights"); + if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("block range concurrency must be an integer greater than or equal to 1"); + if (to < from) return []; + + const heights = Array.from({ length: to - from + 1 }, (_, index) => from + index); + const bundles = new Map(); + let nextIndex = 0; + let failed = false; + + async function worker(): Promise { + for (;;) { + if (failed) return; + const index = nextIndex; + nextIndex += 1; + const height = heights[index]; + if (height === undefined) return; + try { + bundles.set(height, await rpc.block(height)); + } catch (error) { + failed = true; + const message = error instanceof Error ? error.message : String(error); + throw new Error(`failed to fetch block ${height}: ${message}`, { cause: error }); + } + } + } + + const workerCount = Math.min(concurrency, heights.length); + await Promise.all(Array.from({ length: workerCount }, () => worker())); + + return heights.map((height) => { + const bundle = bundles.get(height); + if (!bundle) throw new Error(`missing fetched block ${height}`); + return bundle; + }); +} diff --git a/indexer/src/candle-worker.ts b/indexer/src/candle-worker.ts new file mode 100644 index 000000000..5f14b3091 --- /dev/null +++ b/indexer/src/candle-worker.ts @@ -0,0 +1,46 @@ +import { hostname } from "node:os"; +import { loadConfig } from "./config.js"; +import { createPool, processNextCandleJob, type PgClient } from "./db.js"; + +const args = new Set(process.argv.slice(2)); +const config = loadConfig(); +const pool = createPool(config); + +const workerId = process.env.CANDLE_WORKER_ID ?? `${hostname()}:${process.pid}`; +const pollMs = Number(process.env.CANDLE_WORKER_POLL_MS ?? config.pollIntervalMs); +const batchSize = Number(process.env.CANDLE_WORKER_BATCH_SIZE ?? 2_147_483_647); +const staleAfterMs = Number(process.env.CANDLE_WORKER_STALE_AFTER_MS ?? 10 * 60 * 1000); + +async function withClient(fn: (client: PgClient) => Promise): Promise { + const client = await pool.connect(); + try { + return await fn(client); + } finally { + client.release(); + } +} + +async function runOnce(): Promise { + const job = await withClient((client) => processNextCandleJob(client, { + chainId: config.chainId, + workerId, + batchSize, + staleAfterMs, + })); + if (!job) return false; + console.log(`candle worker processed job=${job.id} pair=${job.pairAddress} from=${job.fromTime} to=${job.toTime}`); + return true; +} + +try { + if (args.has("--once")) { + await runOnce(); + } else { + for (;;) { + const processed = await runOnce(); + if (!processed) await new Promise((resolve) => setTimeout(resolve, pollMs)); + } + } +} finally { + await pool.end(); +} diff --git a/indexer/src/candles.ts b/indexer/src/candles.ts new file mode 100644 index 000000000..fe08d83d0 --- /dev/null +++ b/indexer/src/candles.ts @@ -0,0 +1,106 @@ +export const SUPPORTED_CANDLE_INTERVALS = ["5m", "1h", "1d"] as const; +export type CandleInterval = typeof SUPPORTED_CANDLE_INTERVALS[number]; + +const INTERVAL_MS: Record = { + "5m": 5 * 60 * 1000, + "1h": 60 * 60 * 1000, + "1d": 24 * 60 * 60 * 1000, +}; + +export type SwapForCandle = { + pairAddress: string; + blockTime: string; + offerAsset?: string; + offerAmount?: string; + askAsset?: string; + returnAmount?: string; +}; + +export type DerivedSwapPrice = { + baseAsset: string; + quoteAsset: string; + price: string; + volume: string; + volumeQuote: string; +}; + +export function isCandleInterval(value: string): value is CandleInterval { + return (SUPPORTED_CANDLE_INTERVALS as readonly string[]).includes(value); +} + +export function bucketStartFor(blockTime: string | Date, interval: CandleInterval): string { + const date = blockTime instanceof Date ? blockTime : new Date(blockTime); + if (Number.isNaN(date.getTime())) throw new Error(`invalid candle timestamp: ${blockTime}`); + return new Date(Math.floor(date.getTime() / INTERVAL_MS[interval]) * INTERVAL_MS[interval]).toISOString(); +} + +function parsePositive(value?: string): number | undefined { + if (!value) return undefined; + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; +} + +function formatDecimal(value: number): string { + if (!Number.isFinite(value)) throw new Error("invalid candle decimal"); + return value.toPrecision(18).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1"); +} + +export function deriveCanonicalSwapPrice(swap: SwapForCandle, decimals: Record = {}): DerivedSwapPrice | undefined { + if (!swap.offerAsset || !swap.askAsset || swap.offerAsset === swap.askAsset) return undefined; + const offerRaw = parsePositive(swap.offerAmount); + const returnRaw = parsePositive(swap.returnAmount); + if (!offerRaw || !returnRaw) return undefined; + + const offer = offerRaw / 10 ** (decimals[swap.offerAsset] ?? 0); + const returned = returnRaw / 10 ** (decimals[swap.askAsset] ?? 0); + if (offer <= 0 || returned <= 0) return undefined; + + const offerIsBase = swap.offerAsset < swap.askAsset; + const baseAsset = offerIsBase ? swap.offerAsset : swap.askAsset; + const quoteAsset = offerIsBase ? swap.askAsset : swap.offerAsset; + const baseVolume = offerIsBase ? offer : returned; + const quoteVolume = offerIsBase ? returned : offer; + const price = quoteVolume / baseVolume; + if (!Number.isFinite(price) || price <= 0) return undefined; + + return { + baseAsset, + quoteAsset, + price: formatDecimal(price), + volume: formatDecimal(baseVolume), + volumeQuote: formatDecimal(quoteVolume), + }; +} + +export function aggregateSwapsToCandles(swaps: SwapForCandle[], interval: CandleInterval, decimals: Record = {}) { + const buckets = new Map(); + for (const swap of swaps) { + const derived = deriveCanonicalSwapPrice(swap, decimals); + if (!derived) continue; + const key = `${swap.pairAddress}:${derived.baseAsset}:${derived.quoteAsset}:${bucketStartFor(swap.blockTime, interval)}`; + const existing = buckets.get(key); + if (!existing) { + buckets.set(key, { + bucketStart: bucketStartFor(swap.blockTime, interval), + open: derived.price, + high: derived.price, + low: derived.price, + close: derived.price, + volume: Number(derived.volume), + volumeQuote: Number(derived.volumeQuote), + tradeCount: 1, + baseAsset: derived.baseAsset, + quoteAsset: derived.quoteAsset, + pairAddress: swap.pairAddress, + }); + } else { + existing.high = formatDecimal(Math.max(Number(existing.high), Number(derived.price))); + existing.low = formatDecimal(Math.min(Number(existing.low), Number(derived.price))); + existing.close = derived.price; + existing.volume += Number(derived.volume); + existing.volumeQuote += Number(derived.volumeQuote); + existing.tradeCount += 1; + } + } + return [...buckets.values()].map((candle) => ({ ...candle, volume: formatDecimal(candle.volume), volumeQuote: formatDecimal(candle.volumeQuote) })); +} diff --git a/indexer/src/config.ts b/indexer/src/config.ts new file mode 100644 index 000000000..728661637 --- /dev/null +++ b/indexer/src/config.ts @@ -0,0 +1,179 @@ +export const DEFAULT_CONTRACTS = { + factory: "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca", + router: "juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s", + incentives: "juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598", + oracle: "juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p", + nativeCoinRegistry: + "juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2", +} as const; + +export const DEFAULT_START_HEIGHT = 39_381_297; + +export type IndexerMode = "realtime" | "catchup"; + +export type IndexerConfig = { + databaseUrl: string; + rpcUrl: string; + restUrl: string; + wsUrl: string; + chainId: string; + factoryAddress: string; + routerAddress: string; + incentivesAddress: string; + oracleAddress: string; + nativeCoinRegistryAddress: string; + startHeight: number; + confirmationDepth: number; + pollIntervalMs: number; + batchSize: number; + dryRun: boolean; + cursorId: string; + indexerMode: IndexerMode; + rangeSize: number; + fetchWindowSize: number; + fetchConcurrency: number; + realtimeFetchConcurrency: number; + rpcTimeoutMs: number; + rpcMaxRetries: number; + ingestCandlesInline: boolean; + ingestReserveSnapshotsInline: boolean; + ingestAggregatesInline: boolean; + ingestBulkStagingEnabled: boolean; + priceProviderBaseUrl?: string; + priceProviderApiKey?: string; + priceProviderName: string; + priceCacheTtlMs: number; + priceStaleAfterMs: number; + priceAllowStale: boolean; + priceDevMocks: boolean; + readModelRefreshIntervalMs: number; + apiPort: number; +}; + +function env(name: string, fallback: string): string { + return process.env[name] ?? fallback; +} + +type IntEnvOptions = { min?: number; label?: string }; + +process.loadEnvFile?.(".env"); + +function intEnv( + name: string, + fallback: number, + options: IntEnvOptions = {}, +): number { + const value = process.env[name]; + if (!value) return fallback; + const trimmed = value.trim(); + if (!/^\d+$/.test(trimmed)) + throw new Error( + `${name} must be ${options.label ?? "a non-negative integer"}`, + ); + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isSafeInteger(parsed) || parsed < (options.min ?? 0)) { + throw new Error( + `${name} must be ${options.label ?? "a non-negative integer"}`, + ); + } + return parsed; +} + +function boolEnv(name: string, fallback = false): boolean { + const value = process.env[name]; + if (!value) return fallback; + return ["1", "true", "yes", "y"].includes(value.toLowerCase()); +} + +function indexerModeEnv(): IndexerMode { + const value = env("INDEXER_MODE", "realtime"); + if (value === "realtime" || value === "catchup") return value; + throw new Error('INDEXER_MODE must be either "realtime" or "catchup"'); +} + +function deriveWsUrl(rpcUrl: string): string { + const url = new URL(rpcUrl); + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; + url.pathname = "/websocket"; + return url.toString(); +} + +export function loadConfig(): IndexerConfig { + const rpcUrl = env( + "JUNO_RPC_URL", + "https://juno-rpc.publicnode.com:443", + ).replace(/\/$/, ""); + const fetchWindowSize = intEnv("FETCH_WINDOW_SIZE", 250, { + min: 1, + label: "an integer greater than or equal to 1", + }); + const fetchConcurrency = intEnv("FETCH_CONCURRENCY", 32, { + min: 1, + label: "an integer greater than or equal to 1", + }); + if (fetchConcurrency > fetchWindowSize) { + throw new Error( + "FETCH_CONCURRENCY must be less than or equal to FETCH_WINDOW_SIZE", + ); + } + + return { + databaseUrl: env( + "DATABASE_URL", + "postgres://postgres:postgres@localhost:5432/astroport_indexer", + ), + rpcUrl, + restUrl: env("JUNO_REST_URL", "https://juno-rest.publicnode.com").replace( + /\/$/, + "", + ), + wsUrl: env("JUNO_WS_URL", deriveWsUrl(rpcUrl)), + chainId: env("CHAIN_ID", "juno-1"), + factoryAddress: env("FACTORY_ADDRESS", DEFAULT_CONTRACTS.factory), + routerAddress: env("ROUTER_ADDRESS", DEFAULT_CONTRACTS.router), + incentivesAddress: env("INCENTIVES_ADDRESS", DEFAULT_CONTRACTS.incentives), + oracleAddress: env("ORACLE_ADDRESS", DEFAULT_CONTRACTS.oracle), + nativeCoinRegistryAddress: env( + "NATIVE_COIN_REGISTRY_ADDRESS", + DEFAULT_CONTRACTS.nativeCoinRegistry, + ), + startHeight: intEnv("START_HEIGHT", DEFAULT_START_HEIGHT), + confirmationDepth: intEnv("CONFIRMATION_DEPTH", 2), + pollIntervalMs: intEnv("POLL_INTERVAL_MS", 5_000), + batchSize: intEnv("BATCH_SIZE", 20, { + min: 1, + label: "an integer greater than or equal to 1", + }), + dryRun: boolEnv("DRY_RUN"), + cursorId: env("CURSOR_ID", "astroport-juno-v1"), + indexerMode: indexerModeEnv(), + rangeSize: intEnv("RANGE_SIZE", 5_000, { + min: 1, + label: "an integer greater than or equal to 1", + }), + fetchWindowSize, + fetchConcurrency, + realtimeFetchConcurrency: intEnv("REALTIME_FETCH_CONCURRENCY", 8, { + min: 1, + label: "an integer greater than or equal to 1", + }), + rpcTimeoutMs: intEnv("RPC_TIMEOUT_MS", 10_000), + rpcMaxRetries: intEnv("RPC_MAX_RETRIES", 5), + ingestCandlesInline: boolEnv("INGEST_CANDLES_INLINE", true), + ingestReserveSnapshotsInline: boolEnv( + "INGEST_RESERVE_SNAPSHOTS_INLINE", + true, + ), + ingestAggregatesInline: boolEnv("INGEST_AGGREGATES_INLINE", false), + ingestBulkStagingEnabled: boolEnv("INGEST_BULK_STAGING_ENABLED", false), + priceProviderBaseUrl: process.env.PRICE_PROVIDER_BASE_URL || undefined, + priceProviderApiKey: process.env.PRICE_PROVIDER_API_KEY || undefined, + priceProviderName: env("PRICE_PROVIDER_NAME", "provider"), + priceCacheTtlMs: intEnv("PRICE_CACHE_TTL_MS", 300_000), + priceStaleAfterMs: intEnv("PRICE_STALE_AFTER_MS", 1_800_000), + priceAllowStale: boolEnv("PRICE_ALLOW_STALE", true), + priceDevMocks: boolEnv("PRICE_DEV_MOCKS"), + readModelRefreshIntervalMs: intEnv("READ_MODEL_REFRESH_INTERVAL_MS", 15_000), + apiPort: intEnv("API_PORT", 8787), + }; +} diff --git a/indexer/src/db.ts b/indexer/src/db.ts new file mode 100644 index 000000000..5d018a2b5 --- /dev/null +++ b/indexer/src/db.ts @@ -0,0 +1,889 @@ +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import pg from "pg"; +import { aggregateSwapsToCandles, bucketStartFor, deriveCanonicalSwapPrice, SUPPORTED_CANDLE_INTERVALS } from "./candles.js"; +import type { IndexerConfig } from "./config.js"; +import type { IncentiveEvent, LiquidityEvent, NormalizedEvent, PoolCreatedEvent, SwapEvent } from "./events.js"; + +const { Pool } = pg; +export type PgPool = InstanceType; +export type PgClient = pg.PoolClient; + +export type SnapshotJobStatus = "pending" | "leased" | "succeeded" | "failed"; +export type SnapshotJob = { + id: string; + chainId: string; + pairAddress: string; + height: number; + blockTime: string; + reason: string; + status: SnapshotJobStatus; + attempts: number; +}; + +export function createPool(config: IndexerConfig): PgPool { + return new Pool({ connectionString: config.databaseUrl, max: 5 }); +} + +const DEFAULT_MIGRATIONS_DIR = join(process.cwd(), "migrations"); + +export async function listMigrationFiles(migrationsDir = DEFAULT_MIGRATIONS_DIR): Promise { + return (await readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort(); +} + +export async function runMigrations(pool: PgPool, migrationsDir = DEFAULT_MIGRATIONS_DIR): Promise { + const files = await listMigrationFiles(migrationsDir); + await pool.query(`CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())`); + const existing = await pool.query<{ version: string }>("SELECT version FROM schema_migrations"); + const alreadyApplied = new Set(existing.rows.map((row) => row.version)); + const applied: string[] = []; + for (const file of files) { + if (alreadyApplied.has(file)) continue; + const sql = await readFile(join(migrationsDir, file), "utf8"); + await pool.query("BEGIN"); + try { + await pool.query(sql); + await pool.query("INSERT INTO schema_migrations(version) VALUES($1) ON CONFLICT DO NOTHING", [file]); + await pool.query("COMMIT"); + applied.push(file); + } catch (error) { + await pool.query("ROLLBACK"); + throw error; + } + } + return applied; +} + +export async function getCursor(client: PgClient, cursorId: string, chainId: string, startHeight: number): Promise { + const result = await client.query<{ last_height: string }>( + `INSERT INTO indexer_cursors(id, chain_id, last_height) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO UPDATE SET updated_at = now() + RETURNING last_height`, + [cursorId, chainId, Math.max(0, startHeight - 1)], + ); + return Number(result.rows[0]?.last_height ?? Math.max(0, startHeight - 1)); +} + +export async function recordProcessedBlock( + client: PgClient, + params: { chainId: string; height: number; blockHash: string; parentHash?: string; blockTime: string; txCount: number }, +): Promise { + const existing = await client.query<{ block_hash: string; parent_hash: string | null }>( + `SELECT block_hash, parent_hash FROM processed_blocks WHERE chain_id = $1 AND height = $2`, + [params.chainId, params.height], + ); + const existingBlock = existing.rows[0]; + if (existingBlock && existingBlock.block_hash !== params.blockHash) { + throw new Error(`processed block hash mismatch at height ${params.height}: existing=${existingBlock.block_hash} incoming=${params.blockHash}`); + } + + if (params.parentHash) { + const previous = await client.query<{ block_hash: string }>( + `SELECT block_hash FROM processed_blocks WHERE chain_id = $1 AND height = $2 - 1`, + [params.chainId, params.height], + ); + const previousHash = previous.rows[0]?.block_hash; + if (previousHash && previousHash !== params.parentHash) { + throw new Error(`processed block parent hash mismatch at height ${params.height}: previous=${previousHash} incoming_parent=${params.parentHash}`); + } + } + + const written = await client.query( + `INSERT INTO processed_blocks(chain_id, height, block_hash, parent_hash, block_time, tx_count) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (height) DO UPDATE + SET chain_id = EXCLUDED.chain_id, + block_time = EXCLUDED.block_time, + tx_count = EXCLUDED.tx_count, + processed_at = now(), + parent_hash = COALESCE(processed_blocks.parent_hash, EXCLUDED.parent_hash) + WHERE processed_blocks.chain_id = EXCLUDED.chain_id + AND processed_blocks.block_hash = EXCLUDED.block_hash + AND ( + processed_blocks.parent_hash IS NULL + OR EXCLUDED.parent_hash IS NULL + OR processed_blocks.parent_hash = EXCLUDED.parent_hash + )`, + [params.chainId, params.height, params.blockHash, params.parentHash ?? null, params.blockTime, params.txCount], + ); + if (written.rowCount === 0) { + throw new Error(`processed block conflict at height ${params.height}: existing row differs from incoming block`); + } +} + +export async function advanceCursor( + client: PgClient, + params: { cursorId: string; height: number; blockHash: string }, +): Promise { + await client.query( + `UPDATE indexer_cursors SET last_height = $2, last_block_hash = $3, updated_at = now() WHERE id = $1`, + [params.cursorId, params.height, params.blockHash], + ); +} + +export async function upsertPoolStateSnapshot( + client: PgClient, + params: { chainId: string; pairAddress: string; height: number; blockTime: string; reserves: unknown[]; totalShare?: string | null; source?: string }, +): Promise { + const pool = await client.query<{ id: string }>(`SELECT id FROM pools WHERE chain_id = $1 AND pair_address = $2`, [params.chainId, params.pairAddress]); + const poolId = pool.rows[0]?.id; + if (!poolId) throw new Error(`cannot write pool state snapshot for unknown pair ${params.pairAddress}`); + await client.query( + `INSERT INTO pool_state_snapshots(pool_id, height, block_time, reserves, total_share, source) + VALUES ($1, $2, $3, $4::jsonb, $5, $6) + ON CONFLICT (pool_id, height, source) DO UPDATE + SET block_time = EXCLUDED.block_time, + reserves = EXCLUDED.reserves, + total_share = EXCLUDED.total_share`, + [poolId, params.height, params.blockTime, JSON.stringify(params.reserves), params.totalShare ?? null, params.source ?? "event"], + ); +} + +export async function enqueueSnapshotJobs( + client: PgClient, + params: { chainId: string; pairAddresses: string[]; height: number; blockTime: string; reason: string }, +): Promise { + const pairAddresses = [...new Set(params.pairAddresses.filter(Boolean))]; + if (pairAddresses.length === 0) return 0; + const result = await client.query( + `INSERT INTO snapshot_jobs(chain_id, pair_address, height, block_time, reason) + SELECT $1, p.pair_address, $3, $4, $5 + FROM pools p + WHERE p.chain_id = $1 + AND p.pair_address = ANY($2::text[]) + ON CONFLICT (chain_id, pair_address, height, reason) DO NOTHING`, + [params.chainId, pairAddresses, params.height, params.blockTime, params.reason], + ); + return result.rowCount ?? 0; +} + +export async function claimSnapshotJobs( + client: PgClient, + params: { chainId: string; limit: number; leaseSeconds: number; maxAttempts: number }, +): Promise { + const result = await client.query<{ + id: string; + chain_id: string; + pair_address: string; + height: string | number; + block_time: string; + reason: string; + status: SnapshotJobStatus; + attempts: string | number; + }>( + `WITH claimable AS ( + SELECT id + FROM snapshot_jobs + WHERE chain_id = $1 + AND status IN ('pending', 'leased') + AND attempts < $4 + AND (status = 'pending' OR leased_until <= now() OR leased_until IS NULL) + ORDER BY id ASC + LIMIT $2 + FOR UPDATE SKIP LOCKED + ) + UPDATE snapshot_jobs j + SET status = 'leased', + attempts = j.attempts + 1, + leased_until = now() + ($3::text)::interval, + updated_at = now() + FROM claimable + WHERE j.id = claimable.id + RETURNING j.id, j.chain_id, j.pair_address, j.height, j.block_time, j.reason, j.status, j.attempts`, + [params.chainId, params.limit, `${params.leaseSeconds} seconds`, params.maxAttempts], + ); + return result.rows.map((row) => ({ + id: String(row.id), + chainId: row.chain_id, + pairAddress: row.pair_address, + height: Number(row.height), + blockTime: row.block_time, + reason: row.reason, + status: row.status, + attempts: Number(row.attempts), + })); +} + +export async function markSnapshotJobSucceeded(client: PgClient, params: { jobId: string; attempt: number }): Promise { + await client.query( + `UPDATE snapshot_jobs + SET status = 'succeeded', leased_until = NULL, last_error = NULL, updated_at = now() + WHERE id = $1 + AND status = 'leased' + AND attempts = $2`, + [params.jobId, params.attempt], + ); +} + +export async function markSnapshotJobFailed( + client: PgClient, + params: { jobId: string; attempt: number; error: string; permanent: boolean; maxAttempts: number }, +): Promise { + await client.query( + `UPDATE snapshot_jobs + SET status = CASE WHEN $3::boolean OR attempts >= $5 THEN 'failed' ELSE 'pending' END, + leased_until = NULL, + last_error = $4, + updated_at = now() + WHERE id = $1 + AND status = 'leased' + AND attempts = $2`, + [params.jobId, params.attempt, params.permanent, params.error.slice(0, 2_000), params.maxAttempts], + ); +} + +export type WriteNormalizedEventsOptions = { + writeCandlesInline?: boolean; +}; + +export type StagedBlock = { + chainId: string; + height: number; + blockHash: string; + parentHash?: string; + blockTime: string; + txCount: number; + events: NormalizedEvent[]; +}; + +type MultiInsertColumn = { name: string; value: (row: T) => unknown; cast?: string }; + +async function multiInsert(client: PgClient, table: string, columns: MultiInsertColumn[], rows: T[], conflict = "DO NOTHING"): Promise { + if (rows.length === 0) return; + const values: unknown[] = []; + const tuples = rows.map((row) => { + const placeholders = columns.map((column) => { + values.push(column.value(row)); + return `$${values.length}${column.cast ? `::${column.cast}` : ""}`; + }); + return `(${placeholders.join(",")})`; + }); + await client.query( + `INSERT INTO ${table}(${columns.map((column) => column.name).join(",")}) VALUES ${tuples.join(",")} ON CONFLICT ${conflict}`, + values, + ); +} + +export async function stageAndMergeBatch( + client: PgClient, + params: { batchId: string; chainId: string; cursorId: string; blocks: StagedBlock[]; writeCandlesInline?: boolean; enqueueSnapshots?: boolean; cleanupOlderThanHours?: number }, +): Promise { + if (params.blocks.length === 0) return; + const batchId = params.batchId; + await stageProcessedBlocks(client, batchId, params.blocks); + await stageEvents(client, batchId, params.chainId, params.blocks.flatMap((block) => block.events)); + await mergeStagedBatch(client, params); +} + +async function stageProcessedBlocks(client: PgClient, batchId: string, blocks: StagedBlock[]): Promise { + await multiInsert(client, "stage_processed_blocks", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: (block) => block.chainId }, + { name: "height", value: (block) => block.height }, + { name: "block_hash", value: (block) => block.blockHash }, + { name: "parent_hash", value: (block) => block.parentHash ?? null }, + { name: "block_time", value: (block) => block.blockTime }, + { name: "tx_count", value: (block) => block.txCount }, + ], blocks); +} + +async function stageEvents(client: PgClient, batchId: string, chainId: string, events: NormalizedEvent[]): Promise { + await multiInsert(client, "stage_pools", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "factory_address", value: (event) => event.factoryAddress }, + { name: "pair_address", value: (event) => event.pairAddress }, + { name: "liquidity_token_address", value: (event) => event.liquidityTokenAddress ?? null }, + { name: "pool_type", value: (event) => event.poolType ?? null }, + { name: "asset_infos", value: (event) => JSON.stringify(event.assetInfos), cast: "jsonb" }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event): event is PoolCreatedEvent => event.kind === "pool_created")); + + await multiInsert(client, "stage_swaps", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "pair_address", value: (event) => event.pairAddress }, + { name: "trader", value: (event) => event.trader ?? null }, + { name: "offer_asset", value: (event) => event.offerAsset ?? null }, + { name: "offer_amount", value: (event) => event.offerAmount ?? null }, + { name: "ask_asset", value: (event) => event.askAsset ?? null }, + { name: "return_amount", value: (event) => event.returnAmount ?? null }, + { name: "spread_amount", value: (event) => event.spreadAmount ?? null }, + { name: "commission_amount", value: (event) => event.commissionAmount ?? null }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event): event is SwapEvent => event.kind === "swap")); + + await multiInsert(client, "stage_liquidity_events", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "pair_address", value: (event) => event.pairAddress }, + { name: "kind", value: (event) => event.kind }, + { name: "provider", value: (event) => event.provider ?? null }, + { name: "assets", value: (event) => JSON.stringify(event.assets), cast: "jsonb" }, + { name: "share_amount", value: (event) => event.shareAmount ?? null }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event): event is LiquidityEvent => event.kind === "provide" || event.kind === "withdraw")); + + await multiInsert(client, "stage_incentive_events", [ + { name: "batch_id", value: () => batchId, cast: "uuid" }, + { name: "chain_id", value: () => chainId }, + { name: "height", value: (event) => event.height }, + { name: "block_time", value: (event) => event.blockTime }, + { name: "tx_hash", value: (event) => event.txHash }, + { name: "msg_index", value: (event) => event.msgIndex }, + { name: "event_index", value: (event) => event.eventIndex }, + { name: "incentives_address", value: (event) => event.incentivesAddress }, + { name: "lp_token_address", value: (event) => event.lpTokenAddress ?? null }, + { name: "user_address", value: (event) => event.userAddress ?? null }, + { name: "action", value: (event) => event.action }, + { name: "amount", value: (event) => event.amount ?? null }, + { name: "reward_asset", value: (event) => event.rewardAsset ?? null }, + { name: "reward_amount", value: (event) => event.rewardAmount ?? null }, + { name: "raw_event", value: (event) => JSON.stringify(event.raw), cast: "jsonb" }, + ], events.filter((event): event is IncentiveEvent => event.kind === "incentive")); +} + +async function mergeStagedBatch( + client: PgClient, + params: { batchId: string; chainId: string; cursorId: string; blocks: StagedBlock[]; writeCandlesInline?: boolean; enqueueSnapshots?: boolean; cleanupOlderThanHours?: number }, +): Promise { + const batchId = params.batchId; + await validateStagedBlockContinuity(client, params.chainId, params.blocks); + const blockResult = await client.query( + `INSERT INTO processed_blocks(chain_id, height, block_hash, parent_hash, block_time, tx_count) + SELECT chain_id, height, block_hash, parent_hash, block_time, tx_count + FROM stage_processed_blocks + WHERE batch_id = $1::uuid AND chain_id = $2 + ORDER BY height ASC + ON CONFLICT (height) DO UPDATE + SET chain_id = EXCLUDED.chain_id, + block_time = EXCLUDED.block_time, + tx_count = EXCLUDED.tx_count, + processed_at = now(), + parent_hash = COALESCE(processed_blocks.parent_hash, EXCLUDED.parent_hash) + WHERE processed_blocks.chain_id = EXCLUDED.chain_id + AND processed_blocks.block_hash = EXCLUDED.block_hash + AND ( + processed_blocks.parent_hash IS NULL + OR EXCLUDED.parent_hash IS NULL + OR processed_blocks.parent_hash = EXCLUDED.parent_hash + )`, + [batchId, params.chainId], + ); + if ((blockResult.rowCount ?? 0) !== params.blocks.length) throw new Error(`processed block conflict while merging staging batch ${batchId}`); + + await client.query( + `INSERT INTO pools(chain_id, pair_address, factory_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, first_seen_at) + SELECT chain_id, pair_address, factory_address, liquidity_token_address, pool_type, asset_infos, height, tx_hash, block_time + FROM stage_pools + WHERE batch_id = $1::uuid AND chain_id = $2 + ORDER BY height ASC, msg_index ASC, event_index ASC + ON CONFLICT (chain_id, pair_address) DO UPDATE + SET liquidity_token_address = COALESCE(EXCLUDED.liquidity_token_address, pools.liquidity_token_address), + pool_type = COALESCE(EXCLUDED.pool_type, pools.pool_type), + asset_infos = CASE WHEN jsonb_array_length(EXCLUDED.asset_infos) > 0 THEN EXCLUDED.asset_infos ELSE pools.asset_infos END, + updated_at = now()`, + [batchId, params.chainId], + ); + + await client.query( + `INSERT INTO swaps(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, trader, + offer_asset, offer_amount, ask_asset, return_amount, spread_amount, commission_amount, raw_event) + SELECT s.chain_id, p.id, s.pair_address, s.height, s.block_time, s.tx_hash, s.msg_index, s.event_index, s.trader, + s.offer_asset, s.offer_amount, s.ask_asset, s.return_amount, s.spread_amount, s.commission_amount, s.raw_event + FROM stage_swaps s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + WHERE s.batch_id = $1::uuid AND s.chain_id = $2 + ORDER BY s.height ASC, s.msg_index ASC, s.event_index ASC + ON CONFLICT DO NOTHING`, + [batchId, params.chainId], + ); + + await client.query( + `INSERT INTO liquidity_events(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, kind, provider, assets, share_amount, raw_event) + SELECT s.chain_id, p.id, s.pair_address, s.height, s.block_time, s.tx_hash, s.msg_index, s.event_index, s.kind, s.provider, s.assets, s.share_amount, s.raw_event + FROM stage_liquidity_events s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + WHERE s.batch_id = $1::uuid AND s.chain_id = $2 + ORDER BY s.height ASC, s.msg_index ASC, s.event_index ASC + ON CONFLICT DO NOTHING`, + [batchId, params.chainId], + ); + + await client.query( + `INSERT INTO incentive_events(chain_id, incentives_address, lp_token_address, user_address, action, amount, reward_asset, reward_amount, + height, block_time, tx_hash, msg_index, event_index, raw_event) + SELECT chain_id, incentives_address, lp_token_address, user_address, action, amount, reward_asset, reward_amount, + height, block_time, tx_hash, msg_index, event_index, raw_event + FROM stage_incentive_events + WHERE batch_id = $1::uuid AND chain_id = $2 + ORDER BY height ASC, msg_index ASC, event_index ASC + ON CONFLICT DO NOTHING`, + [batchId, params.chainId], + ); + + if (params.writeCandlesInline === false) { + await client.query( + `INSERT INTO candle_jobs(chain_id, pair_address, from_time, to_time, status, run_after) + SELECT DISTINCT s.chain_id, s.pair_address, + date_trunc('day', s.block_time AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' AS from_time, + (date_trunc('day', s.block_time AT TIME ZONE 'UTC') AT TIME ZONE 'UTC') + interval '1 day' AS to_time, + 'pending', now() + FROM stage_swaps s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + WHERE s.batch_id = $1::uuid AND s.chain_id = $2 + ON CONFLICT (chain_id, pair_address, from_time, to_time) DO UPDATE + SET status = CASE WHEN candle_jobs.status = 'running' THEN candle_jobs.status ELSE 'pending' END, + rerun_requested = CASE WHEN candle_jobs.status = 'running' THEN true ELSE false END, + run_after = now(), + last_error = NULL, + updated_at = now()`, + [batchId, params.chainId], + ); + } + + if (params.enqueueSnapshots) { + await client.query( + `INSERT INTO snapshot_jobs(chain_id, pair_address, height, block_time, reason) + SELECT DISTINCT s.chain_id, s.pair_address, s.height, s.block_time, 'touched' + FROM ( + SELECT chain_id, pair_address, height, block_time FROM stage_swaps WHERE batch_id = $1::uuid AND chain_id = $2 + UNION + SELECT chain_id, pair_address, height, block_time FROM stage_liquidity_events WHERE batch_id = $1::uuid AND chain_id = $2 + ) s + JOIN pools p ON p.chain_id = s.chain_id AND p.pair_address = s.pair_address + ON CONFLICT (chain_id, pair_address, height, reason) DO NOTHING`, + [batchId, params.chainId], + ); + } + + const lastBlock = params.blocks[params.blocks.length - 1]; + await advanceCursor(client, { cursorId: params.cursorId, height: lastBlock.height, blockHash: lastBlock.blockHash }); + await client.query(`UPDATE stage_processed_blocks SET merged_at = now() WHERE batch_id = $1::uuid AND chain_id = $2`, [batchId, params.chainId]); + await cleanupSuccessfulStagingBatches(client, { chainId: params.chainId, olderThanHours: params.cleanupOlderThanHours ?? 24 }); +} + +async function validateStagedBlockContinuity(client: PgClient, chainId: string, blocks: StagedBlock[]): Promise { + const ordered = [...blocks].sort((a, b) => a.height - b.height); + for (let index = 1; index < ordered.length; index += 1) { + const previous = ordered[index - 1]; + const current = ordered[index]; + if (current.height !== previous.height + 1) throw new Error(`non-contiguous staging batch at height ${current.height}`); + if (current.parentHash && current.parentHash !== previous.blockHash) { + throw new Error(`parent hash mismatch for staged block ${current.height}`); + } + } + + const first = ordered[0]; + if (!first?.parentHash) return; + const result = await client.query<{ block_hash: string }>( + `SELECT block_hash FROM processed_blocks WHERE chain_id = $1 AND height = $2`, + [chainId, first.height - 1], + ); + const previous = result.rows[0]; + if (previous && previous.block_hash !== first.parentHash) { + throw new Error(`parent hash mismatch for staged block ${first.height}`); + } +} + +export async function cleanupSuccessfulStagingBatches(client: PgClient, params: { chainId: string; olderThanHours?: number }): Promise { + const olderThan = `${params.olderThanHours ?? 24} hours`; + await client.query( + `WITH old_batches AS ( + SELECT DISTINCT batch_id FROM stage_processed_blocks + WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval + ) + DELETE FROM stage_pools WHERE batch_id IN (SELECT batch_id FROM old_batches)`, + [params.chainId, olderThan], + ); + await client.query(`DELETE FROM stage_swaps WHERE batch_id IN (SELECT batch_id FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_liquidity_events WHERE batch_id IN (SELECT batch_id FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_incentive_events WHERE batch_id IN (SELECT batch_id FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval)`, [params.chainId, olderThan]); + await client.query(`DELETE FROM stage_processed_blocks WHERE chain_id = $1 AND merged_at IS NOT NULL AND merged_at < now() - ($2::text)::interval`, [params.chainId, olderThan]); +} + +export async function writeNormalizedEvents(client: PgClient, chainId: string, events: NormalizedEvent[], options: WriteNormalizedEventsOptions = {}): Promise { + for (const event of events) { + if (event.kind === "pool_created") await upsertPool(client, chainId, event); + } + for (const event of events) { + if (event.kind !== "pool_created") await writeNormalizedEvent(client, chainId, event, options); + } +} + +export async function writeNormalizedEvent(client: PgClient, chainId: string, event: NormalizedEvent, options: WriteNormalizedEventsOptions = {}): Promise { + switch (event.kind) { + case "pool_created": + return upsertPool(client, chainId, event); + case "swap": + return insertSwap(client, chainId, event, options); + case "provide": + case "withdraw": + return insertLiquidityEvent(client, chainId, event); + case "incentive": + return insertIncentiveEvent(client, chainId, event); + } +} + +async function upsertPool(client: PgClient, chainId: string, event: PoolCreatedEvent): Promise { + await client.query( + `INSERT INTO pools(chain_id, pair_address, factory_address, liquidity_token_address, pool_type, asset_infos, created_height, created_tx_hash, first_seen_at) + VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9) + ON CONFLICT (chain_id, pair_address) DO UPDATE + SET liquidity_token_address = COALESCE(EXCLUDED.liquidity_token_address, pools.liquidity_token_address), + pool_type = COALESCE(EXCLUDED.pool_type, pools.pool_type), + asset_infos = CASE WHEN jsonb_array_length(EXCLUDED.asset_infos) > 0 THEN EXCLUDED.asset_infos ELSE pools.asset_infos END, + updated_at = now()`, + [chainId, event.pairAddress, event.factoryAddress, event.liquidityTokenAddress ?? null, event.poolType ?? null, JSON.stringify(event.assetInfos), event.height, event.txHash, event.blockTime], + ); +} + +async function poolIdForPair(client: PgClient, chainId: string, pairAddress: string): Promise { + const pool = await client.query<{ id: string }>(`SELECT id FROM pools WHERE chain_id = $1 AND pair_address = $2`, [chainId, pairAddress]); + return pool.rows[0]?.id ?? null; +} + +async function insertSwap(client: PgClient, chainId: string, event: SwapEvent, options: WriteNormalizedEventsOptions): Promise { + const poolId = await poolIdForPair(client, chainId, event.pairAddress); + if (!poolId) return; + const inserted = await client.query<{ id: string; pool_id: string | null }>( + `INSERT INTO swaps(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, trader, + offer_asset, offer_amount, ask_asset, return_amount, spread_amount, commission_amount, raw_event) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16::jsonb) + ON CONFLICT DO NOTHING + RETURNING id, pool_id`, + [ + chainId, + poolId, + event.pairAddress, + event.height, + event.blockTime, + event.txHash, + event.msgIndex, + event.eventIndex, + event.trader ?? null, + event.offerAsset ?? null, + event.offerAmount ?? null, + event.askAsset ?? null, + event.returnAmount ?? null, + event.spreadAmount ?? null, + event.commissionAmount ?? null, + JSON.stringify(event.raw), + ], + ); + if (inserted.rowCount === 0) return; + if (options.writeCandlesInline === false) { + await enqueueCandleJobForSwap(client, chainId, event.pairAddress, event.blockTime); + } else { + await upsertCandlesForSwap(client, chainId, event, poolId); + } +} + +function addMilliseconds(iso: string, ms: number): string { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) throw new Error(`invalid candle job timestamp: ${iso}`); + return new Date(date.getTime() + ms).toISOString(); +} + +export async function enqueueCandleJobForSwap(client: PgClient, chainId: string, pairAddress: string, blockTime: string): Promise { + const fromTime = bucketStartFor(blockTime, "1d"); + const toTime = addMilliseconds(fromTime, 24 * 60 * 60 * 1000); + await client.query( + `INSERT INTO candle_jobs(chain_id, pair_address, from_time, to_time, status, run_after) + VALUES ($1, $2, $3, $4, 'pending', now()) + ON CONFLICT (chain_id, pair_address, from_time, to_time) DO UPDATE + SET status = CASE WHEN candle_jobs.status = 'running' THEN candle_jobs.status ELSE 'pending' END, + rerun_requested = CASE WHEN candle_jobs.status = 'running' THEN true ELSE false END, + run_after = now(), + last_error = NULL, + updated_at = now()`, + [chainId, pairAddress, fromTime, toTime], + ); +} + +const MAX_ASSET_DECIMALS = 36; +const assetDecimalsCache = new Map(); + +function decimalsCacheKey(chainId: string, asset: string) { + return `${chainId}:${asset}`; +} + +function isValidAssetDecimals(value: unknown): value is number | string { + if (value === null || value === undefined || value === "") return false; + const parsed = Number(value); + return Number.isInteger(parsed) && parsed >= 0 && parsed <= MAX_ASSET_DECIMALS; +} + +function hasCompleteDecimals(decimals: Record, assets: Array): boolean { + return assets.every((asset) => Boolean(asset) && decimals[asset!] !== undefined); +} + +async function loadAssetDecimals(client: PgClient, chainId: string, assets: Array): Promise> { + const uniqueAssets = [...new Set(assets.filter((asset): asset is string => Boolean(asset)))]; + if (uniqueAssets.length === 0) return {}; + const decimals: Record = {}; + const missing: string[] = []; + for (const asset of uniqueAssets) { + const cached = assetDecimalsCache.get(decimalsCacheKey(chainId, asset)); + if (cached === undefined) missing.push(asset); + else decimals[asset] = cached; + } + if (missing.length > 0) { + const result = await client.query<{ asset: string; decimals: number | string | null }>( + `SELECT asset, decimals FROM asset_metadata WHERE chain_id = $1 AND asset = ANY($2::text[])`, + [chainId, missing], + ); + for (const row of result.rows) { + if (!row.asset || !isValidAssetDecimals(row.decimals)) continue; + const parsed = Number(row.decimals); + assetDecimalsCache.set(decimalsCacheKey(chainId, row.asset), parsed); + decimals[row.asset] = parsed; + } + } + return decimals; +} + +async function upsertCandlesForSwap(client: PgClient, chainId: string, event: SwapEvent, poolId: string): Promise { + const decimals = await loadAssetDecimals(client, chainId, [event.offerAsset, event.askAsset]); + if (!hasCompleteDecimals(decimals, [event.offerAsset, event.askAsset])) return; + const derived = deriveCanonicalSwapPrice({ + pairAddress: event.pairAddress, + blockTime: event.blockTime, + offerAsset: event.offerAsset, + offerAmount: event.offerAmount, + askAsset: event.askAsset, + returnAmount: event.returnAmount, + }, decimals); + if (!derived) return; + for (const interval of SUPPORTED_CANDLE_INTERVALS) { + await client.query( + `INSERT INTO token_candles(chain_id, pool_id, pair_address, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, volume_usd, trade_count, source) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$8,$8,$8,$9,$10,NULL,1,'indexer') + ON CONFLICT (chain_id, pair_address, asset, quote_asset, interval, bucket_start) DO UPDATE + SET high = GREATEST(token_candles.high, EXCLUDED.high), + low = LEAST(token_candles.low, EXCLUDED.low), + close = EXCLUDED.close, + volume = token_candles.volume + EXCLUDED.volume, + volume_quote = COALESCE(token_candles.volume_quote, 0) + COALESCE(EXCLUDED.volume_quote, 0), + volume_usd = NULL, + trade_count = token_candles.trade_count + 1, + pool_id = COALESCE(token_candles.pool_id, EXCLUDED.pool_id), + updated_at = now()`, + [chainId, poolId, event.pairAddress, derived.baseAsset, derived.quoteAsset, interval, bucketStartFor(event.blockTime, interval), derived.price, derived.volume, derived.volumeQuote], + ); + } +} + +export async function backfillTokenCandles( + client: PgClient, + params: { chainId: string; pairAddress?: string; from?: string; to?: string; batchSize?: number } = { chainId: "juno-1" }, +): Promise { + return rebuildTokenCandlesForRange(client, { ...params, source: "backfill", toExclusive: false }); +} + +export async function rebuildTokenCandlesForRange( + client: PgClient, + params: { chainId: string; pairAddress?: string; from?: string; to?: string; batchSize?: number; source?: string; toExclusive?: boolean } = { chainId: "juno-1" }, +): Promise { + type SwapBackfillRow = { pair_address: string; block_time: string; offer_asset?: string; offer_amount?: string; ask_asset?: string; return_amount?: string; height: string; tx_hash: string; msg_index: string; event_index: string }; + const result = await client.query( + `SELECT pair_address, block_time, offer_asset, offer_amount, ask_asset, return_amount, + $1::text AS chain_id, height, tx_hash, msg_index, event_index + FROM swaps + WHERE chain_id = $1 + AND ($2::text IS NULL OR pair_address = $2) + AND ($3::timestamptz IS NULL OR block_time >= $3) + AND ($4::timestamptz IS NULL OR (($6::boolean AND block_time < $4) OR (NOT $6::boolean AND block_time <= $4))) + ORDER BY height ASC, msg_index ASC, event_index ASC, id ASC + LIMIT $5`, + [params.chainId, params.pairAddress ?? null, params.from ?? null, params.to ?? null, params.batchSize ?? 10_000, params.toExclusive ?? false], + ); + const swaps: Array<{ pairAddress: string; blockTime: string; offerAsset?: string; offerAmount?: string; askAsset?: string; returnAmount?: string }> = result.rows.map((row: SwapBackfillRow) => ({ + pairAddress: row.pair_address, + blockTime: row.block_time, + offerAsset: row.offer_asset, + offerAmount: row.offer_amount, + askAsset: row.ask_asset, + returnAmount: row.return_amount, + })); + const poolIds = new Map(); + for (const row of result.rows) { + if (poolIds.has(row.pair_address)) continue; + const pool = await client.query<{ id: string }>(`SELECT id FROM pools WHERE chain_id = $1 AND pair_address = $2`, [params.chainId, row.pair_address]); + poolIds.set(row.pair_address, pool.rows[0]?.id ?? null); + } + const decimals = await loadAssetDecimals(client, params.chainId, swaps.flatMap((swap) => [swap.offerAsset, swap.askAsset])); + const swapsWithDecimals = swaps.filter((swap) => hasCompleteDecimals(decimals, [swap.offerAsset, swap.askAsset])); + for (const interval of SUPPORTED_CANDLE_INTERVALS) { + const candles = aggregateSwapsToCandles(swapsWithDecimals, interval, decimals); + for (const candle of candles) { + await client.query( + `INSERT INTO token_candles(chain_id, pool_id, pair_address, asset, quote_asset, interval, bucket_start, open, high, low, close, volume, volume_quote, volume_usd, trade_count, source) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,NULL,$14,$15) + ON CONFLICT (chain_id, pair_address, asset, quote_asset, interval, bucket_start) DO UPDATE + SET open = EXCLUDED.open, + high = EXCLUDED.high, + low = EXCLUDED.low, + close = EXCLUDED.close, + volume = EXCLUDED.volume, + volume_quote = EXCLUDED.volume_quote, + volume_usd = NULL, + trade_count = EXCLUDED.trade_count, + pool_id = COALESCE(token_candles.pool_id, EXCLUDED.pool_id), + source = EXCLUDED.source, + updated_at = now()`, + [params.chainId, poolIds.get(candle.pairAddress) ?? null, candle.pairAddress, candle.baseAsset, candle.quoteAsset, interval, candle.bucketStart, candle.open, candle.high, candle.low, candle.close, candle.volume, candle.volumeQuote, candle.tradeCount, params.source ?? "backfill"], + ); + } + } + return result.rowCount ?? 0; +} + +export type CandleJob = { + id: string; + chainId: string; + pairAddress: string; + fromTime: string; + toTime: string; + attempts: number; + workerId: string; +}; + +type CandleJobRow = { id: string; chain_id: string; pair_address: string; from_time: string; to_time: string; attempts: number | string; worker_id: string }; + +function mapCandleJob(row: CandleJobRow): CandleJob { + return { + id: String(row.id), + chainId: row.chain_id, + pairAddress: row.pair_address, + fromTime: row.from_time, + toTime: row.to_time, + attempts: Number(row.attempts), + workerId: row.worker_id, + }; +} + +export async function claimNextCandleJob(client: PgClient, params: { chainId: string; workerId: string; staleAfterMs?: number }): Promise { + const result = await client.query( + `WITH next_job AS ( + SELECT id + FROM candle_jobs + WHERE chain_id = $1 + AND run_after <= now() + AND ( + status IN ('pending', 'failed') + OR (status = 'running' AND claimed_at < now() - (($3::text)::interval)) + ) + ORDER BY run_after ASC, created_at ASC, id ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED + ) + UPDATE candle_jobs + SET status = 'running', + attempts = attempts + 1, + worker_id = $2, + claimed_at = now(), + last_error = NULL, + updated_at = now() + FROM next_job + WHERE candle_jobs.id = next_job.id + RETURNING candle_jobs.id, chain_id, pair_address, from_time, to_time, attempts, worker_id`, + [params.chainId, params.workerId, `${params.staleAfterMs ?? 10 * 60 * 1000} milliseconds`], + ); + const row = result.rows[0]; + return row ? mapCandleJob(row) : undefined; +} + +export async function completeCandleJob(client: PgClient, job: CandleJob, processedSwaps: number): Promise { + await client.query( + `UPDATE candle_jobs + SET status = CASE WHEN rerun_requested THEN 'pending' ELSE 'completed' END, + rerun_requested = false, + processed_swaps = $4, + updated_at = now() + WHERE id = $1 + AND status = 'running' + AND worker_id = $2 + AND attempts = $3`, + [job.id, job.workerId, job.attempts, processedSwaps], + ); +} + +export async function failCandleJob(client: PgClient, job: CandleJob, error: unknown): Promise { + const message = error instanceof Error ? error.message : String(error); + await client.query( + `UPDATE candle_jobs + SET status = 'failed', last_error = $4, run_after = now() + (($5::text)::interval), updated_at = now() + WHERE id = $1 + AND status = 'running' + AND worker_id = $2 + AND attempts = $3`, + [job.id, job.workerId, job.attempts, message.slice(0, 2_000), "30 seconds"], + ); +} + +export async function processNextCandleJob(client: PgClient, params: { chainId: string; workerId: string; batchSize?: number; staleAfterMs?: number }): Promise { + const job = await claimNextCandleJob(client, params); + if (!job) return undefined; + try { + const processed = await rebuildTokenCandlesForRange(client, { + chainId: job.chainId, + pairAddress: job.pairAddress, + from: job.fromTime, + to: job.toTime, + batchSize: params.batchSize ?? 2_147_483_647, + source: "worker", + toExclusive: true, + }); + await completeCandleJob(client, job, processed); + return job; + } catch (error) { + await failCandleJob(client, job, error); + throw error; + } +} + +export async function refreshApiReadModels(client: PgClient, params: { chainId?: string } = {}): Promise> { + const result = await client.query<{ model: string; rows_affected: string | number }>( + `SELECT model, rows_affected FROM refresh_api_read_models($1::text)`, + [params.chainId ?? null], + ); + return result.rows.map((row) => ({ model: row.model, rowsAffected: Number(row.rows_affected) })); +} + +async function insertLiquidityEvent(client: PgClient, chainId: string, event: LiquidityEvent): Promise { + const poolId = await poolIdForPair(client, chainId, event.pairAddress); + if (!poolId) return; + await client.query( + `INSERT INTO liquidity_events(chain_id, pool_id, pair_address, height, block_time, tx_hash, msg_index, event_index, kind, provider, assets, share_amount, raw_event) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13::jsonb) + ON CONFLICT DO NOTHING`, + [chainId, poolId, event.pairAddress, event.height, event.blockTime, event.txHash, event.msgIndex, event.eventIndex, event.kind, event.provider ?? null, JSON.stringify(event.assets), event.shareAmount ?? null, JSON.stringify(event.raw)], + ); +} + +async function insertIncentiveEvent(client: PgClient, chainId: string, event: IncentiveEvent): Promise { + await client.query( + `INSERT INTO incentive_events(chain_id, incentives_address, lp_token_address, user_address, action, amount, reward_asset, reward_amount, + height, block_time, tx_hash, msg_index, event_index, raw_event) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb) + ON CONFLICT DO NOTHING`, + [chainId, event.incentivesAddress, event.lpTokenAddress ?? null, event.userAddress ?? null, event.action, event.amount ?? null, event.rewardAsset ?? null, event.rewardAmount ?? null, event.height, event.blockTime, event.txHash, event.msgIndex, event.eventIndex, JSON.stringify(event.raw)], + ); +} diff --git a/indexer/src/events.ts b/indexer/src/events.ts new file mode 100644 index 000000000..3efa758db --- /dev/null +++ b/indexer/src/events.ts @@ -0,0 +1,219 @@ +export type EventAttribute = { key: string; value: string; index?: boolean }; +export type TendermintEvent = { type: string; attributes: EventAttribute[] }; + +export type EventContext = { + chainId: string; + height: number; + blockTime: string; + txHash: string; + msgIndex: number; + eventIndex: number; +}; + +export type AssetAmount = { asset: string; amount?: string }; + +export type NormalizedEvent = + | PoolCreatedEvent + | SwapEvent + | LiquidityEvent + | IncentiveEvent; + +export type PoolCreatedEvent = EventContext & { + kind: "pool_created"; + factoryAddress: string; + pairAddress: string; + liquidityTokenAddress?: string; + poolType?: string; + assetInfos: string[]; + raw: Record; +}; + +export type SwapEvent = EventContext & { + kind: "swap"; + pairAddress: string; + trader?: string; + offerAsset?: string; + offerAmount?: string; + askAsset?: string; + returnAmount?: string; + spreadAmount?: string; + commissionAmount?: string; + raw: Record; +}; + +export type LiquidityEvent = EventContext & { + kind: "provide" | "withdraw"; + pairAddress: string; + provider?: string; + assets: AssetAmount[]; + shareAmount?: string; + raw: Record; +}; + +export type IncentiveEvent = EventContext & { + kind: "incentive"; + incentivesAddress: string; + action: string; + lpTokenAddress?: string; + userAddress?: string; + amount?: string; + rewardAsset?: string; + rewardAmount?: string; + raw: Record; +}; + +export type ContractAddresses = { + factoryAddress: string; + incentivesAddress: string; +}; + +export function attributesToRecord(attributes: EventAttribute[]): Record { + const out: Record = {}; + for (const { key, value } of attributes) { + const current = out[key]; + if (current === undefined) out[key] = value; + else if (Array.isArray(current)) current.push(value); + else out[key] = [current, value]; + } + return out; +} + +function first(raw: Record, keys: string[]): string | undefined { + for (const key of keys) { + const value = raw[key]; + if (Array.isArray(value)) return value[0]; + if (value) return value; + } + return undefined; +} + +function all(raw: Record, keys: string[]): string[] { + const values: string[] = []; + for (const key of keys) { + const value = raw[key]; + if (Array.isArray(value)) values.push(...value); + else if (value) values.push(value); + } + return values; +} + +function parseAssets(raw: Record): AssetAmount[] { + const denoms = all(raw, ["assets", "asset", "withdrawn_assets", "refund_assets", "provided_assets", "offer_asset", "ask_asset"]); + const amounts = all(raw, ["amounts", "amount", "withdrawn_amounts", "provided_amounts"]); + if (denoms.length === 0 && amounts.length === 0) return []; + if (denoms.length === 1 && amounts.length === 0) return parseCoinList(denoms[0]); + if (denoms.length === amounts.length) return denoms.map((asset, index) => ({ asset, amount: amounts[index] })); + return denoms.map((asset) => ({ asset })); +} + +function parseCoinList(value: string): AssetAmount[] { + return value.split(",").map((part) => part.trim()).filter(Boolean).map((coin) => { + const match = coin.match(/^(\d+)(.+)$/); + return match ? { amount: match[1], asset: match[2] } : { asset: coin }; + }); +} + +function isWasm(event: TendermintEvent): boolean { + return event.type === "wasm" || event.type.startsWith("wasm-") || event.type === "execute"; +} + +export function normalizeWasmEvent( + event: TendermintEvent, + context: EventContext, + contracts: ContractAddresses, +): NormalizedEvent | undefined { + if (!isWasm(event)) return undefined; + const raw = attributesToRecord(event.attributes); + const action = first(raw, ["action", "method", "_contract_action"]); + const contract = first(raw, ["_contract_address", "contract_address"]); + + if (!action || !contract) return undefined; + + if (contract === contracts.factoryAddress && ["create_pair", "pair_created", "create_pair_and_distribution_flows", "register"].includes(action)) { + const pairAddress = first(raw, ["pair_contract_addr", "pair_address", "contract_addr", "pair"]); + if (!pairAddress || !pairAddress.startsWith("juno1")) return undefined; + return { + ...context, + kind: "pool_created", + factoryAddress: contract, + pairAddress, + liquidityTokenAddress: first(raw, ["liquidity_token_addr", "liquidity_token", "lp_token_addr"]), + poolType: first(raw, ["pair_type", "pool_type"]), + assetInfos: all(raw, ["asset_infos", "asset_info", "assets"]), + raw, + }; + } + + if (["swap", "swap_and_send"].includes(action)) { + return { + ...context, + kind: "swap", + pairAddress: contract, + trader: first(raw, ["sender", "trader", "receiver"]), + offerAsset: first(raw, ["offer_asset", "offer_asset_info", "ask_asset"]), + offerAmount: first(raw, ["offer_amount", "amount"]), + askAsset: first(raw, ["ask_asset", "ask_asset_info", "return_asset"]), + returnAmount: first(raw, ["return_amount", "return"]), + spreadAmount: first(raw, ["spread_amount"]), + commissionAmount: first(raw, ["commission_amount"]), + raw, + }; + } + + if (["provide_liquidity", "provide"].includes(action)) { + return { + ...context, + kind: "provide", + pairAddress: contract, + provider: first(raw, ["sender", "provider", "receiver"]), + assets: parseAssets(raw), + shareAmount: first(raw, ["share", "share_amount", "minted_share"]), + raw, + }; + } + + if (["withdraw_liquidity", "withdraw"].includes(action)) { + return { + ...context, + kind: "withdraw", + pairAddress: contract, + provider: first(raw, ["sender", "provider", "receiver"]), + assets: parseAssets(raw), + shareAmount: first(raw, ["share", "share_amount", "refund_share", "withdrawn_share"]), + raw, + }; + } + + if (contract === contracts.incentivesAddress) { + return { + ...context, + kind: "incentive", + incentivesAddress: contract, + action, + lpTokenAddress: first(raw, ["lp_token", "lp_token_addr", "staking_token", "staking_token_addr"]), + userAddress: first(raw, ["user", "sender", "staker", "recipient"]), + amount: first(raw, ["amount", "bond_amount", "unbond_amount"]), + rewardAsset: first(raw, ["reward_asset", "reward_token", "asset"]), + rewardAmount: first(raw, ["reward_amount", "rewards", "amount"]), + raw, + }; + } + + return undefined; +} + +export function normalizeBlockEvents( + events: TendermintEvent[], + baseContext: Omit, + contracts: ContractAddresses, +): NormalizedEvent[] { + return events + .map((event, eventIndex) => normalizeWasmEvent(event, { ...baseContext, msgIndex: inferMsgIndex(event), eventIndex }, contracts)) + .filter((event): event is NormalizedEvent => event !== undefined); +} + +function inferMsgIndex(event: TendermintEvent): number { + const raw = attributesToRecord(event.attributes); + const value = first(raw, ["msg_index", "msg_index_start"]); + return value ? Number.parseInt(value, 10) || 0 : 0; +} diff --git a/indexer/src/index.ts b/indexer/src/index.ts new file mode 100644 index 000000000..861f546a2 --- /dev/null +++ b/indexer/src/index.ts @@ -0,0 +1,38 @@ +import { createIndexerApi } from "./api.js"; +import { PostgresApiStore } from "./api-store.js"; +import { loadConfig } from "./config.js"; +import { createPool, listMigrationFiles, runMigrations } from "./db.js"; +import { Indexer } from "./indexer.js"; +import { indexerMetrics } from "./metrics.js"; +import { ReadModelRefresher } from "./read-model-refresher.js"; + +const config = loadConfig(); +const pool = createPool(config); +const appliedMigrations = await runMigrations(pool); +console.log(`migrations checked: ${appliedMigrations.join(", ")}`); +const expectedMigrationVersions = await listMigrationFiles(); +const api = createIndexerApi(new PostgresApiStore(pool, config.chainId, config.cursorId, { rpcUrl: config.rpcUrl, expectedMigrationVersions, confirmationDepth: config.confirmationDepth }), indexerMetrics); +const indexer = new Indexer(config, pool, indexerMetrics); +const readModels = new ReadModelRefresher(pool, { chainId: config.chainId, intervalMs: config.readModelRefreshIntervalMs }); + +await readModels.refreshOnce().catch((error) => { + console.warn("indexer_read_models_initial_refresh_failed", { error: error instanceof Error ? error.message : String(error) }); +}); +readModels.start(); + +await new Promise((resolve) => api.listen(config.apiPort, resolve)); +console.log(`astroport juno indexer api listening on :${config.apiPort}`); + +async function shutdown(signal: string) { + console.log(`received ${signal}; shutting down indexer`); + readModels.stop(); + await new Promise((resolve, reject) => api.close((error) => (error ? reject(error) : resolve()))); + await indexer.close(); + await pool.end(); + process.exit(0); +} + +process.on("SIGINT", () => void shutdown("SIGINT")); +process.on("SIGTERM", () => void shutdown("SIGTERM")); + +await indexer.runForever(); diff --git a/indexer/src/indexer.ts b/indexer/src/indexer.ts new file mode 100644 index 000000000..f590fd69a --- /dev/null +++ b/indexer/src/indexer.ts @@ -0,0 +1,278 @@ +import { randomUUID } from "node:crypto"; +import type { IndexerConfig } from "./config.js"; +import { fetchBlockRange } from "./block-fetcher.js"; +import { advanceCursor, createPool, enqueueSnapshotJobs, getCursor, recordProcessedBlock, stageAndMergeBatch, upsertPoolStateSnapshot, writeNormalizedEvents, type PgClient, type PgPool } from "./db.js"; +import { normalizeBlockEvents, type NormalizedEvent } from "./events.js"; +import type { IndexerMetrics, WriterEventKind } from "./metrics.js"; +import { JunoRestClient, JunoRpcClient, type BlockBundle, type ChainHead } from "./rpc.js"; +import { nextBlockRange } from "./ranges.js"; + +type PlannedRange = { head: ChainHead; target: number; lastHeight: number; from: number; to: number; empty: boolean }; +type DecodedBlock = { block: BlockBundle; events: NormalizedEvent[] }; + +export class Indexer { + private readonly rpc: JunoRpcClient; + private readonly rest: JunoRestClient; + private readonly pool?: PgPool; + private readonly ownsPool: boolean; + + constructor(private readonly config: IndexerConfig, pool?: PgPool, private readonly metrics?: IndexerMetrics) { + this.rpc = new JunoRpcClient(config.rpcUrl, { metrics, timeoutMs: config.rpcTimeoutMs, maxRetries: config.rpcMaxRetries }); + this.rest = new JunoRestClient(config.restUrl); + this.pool = pool ?? (config.dryRun ? undefined : createPool(config)); + this.ownsPool = !pool && !config.dryRun; + } + + async close(): Promise { + if (this.ownsPool) await this.pool?.end(); + } + + async runOnce(maxHeight?: number): Promise<{ processed: number; head: number; target: number; cursorHeight: number }> { + const planned = await this.planRange(maxHeight); + if (planned.empty) return { processed: 0, head: planned.head.height, target: planned.target, cursorHeight: planned.lastHeight }; + + const rangeStartedAt = Date.now(); + const blocks = await this.fetchRange(planned.from, planned.to); + const decoded = blocks.map((block) => this.normalizeBlock(block)); + for (const _block of decoded) this.metrics?.recordDecodedBlock(); + const eventCounts = countRangeEvents(decoded); + + let dbDurationMs = 0; + const writeStartedAt = Date.now(); + let processed = 0; + try { + processed = this.shouldUseBulkStaging() + ? await this.writeBulkStagingBatch(decoded) + : await this.writeBlocksInOrder(decoded); + dbDurationMs = Date.now() - writeStartedAt; + } catch (error) { + dbDurationMs = Date.now() - writeStartedAt; + if (isReorgHaltError(error)) this.metrics?.setReorgHalt(true); + throw error; + } + + if (!this.config.dryRun) { + this.metrics?.recordWriterEvents(eventCounts); + if (processed > 0) this.metrics?.recordWriterBlock(dbDurationMs / 1000); + } + this.metrics?.setReorgHalt(false); + console.log(JSON.stringify({ + msg: "indexer_range_processed", + role: "indexer", + rangeFrom: planned.from, + rangeTo: planned.to, + cursor: planned.to, + head: planned.head.height, + target: planned.target, + lag: Math.max(0, planned.target - planned.to), + blocks: processed, + swaps: eventCounts.swap ?? 0, + liquidityEvents: (eventCounts.provide ?? 0) + (eventCounts.withdraw ?? 0), + incentiveEvents: eventCounts.incentive ?? 0, + durationMs: Date.now() - rangeStartedAt, + dbDurationMs, + })); + + return { processed, head: planned.head.height, target: planned.target, cursorHeight: planned.to }; + } + + async runForever(): Promise { + for (;;) { + const result = await this.runOnce(); + console.log(`indexer loop processed=${result.processed} head=${result.head} target=${result.target}`); + await new Promise((resolve) => setTimeout(resolve, this.config.pollIntervalMs)); + } + } + + async runUntilHeight(maxHeight: number): Promise<{ processed: number; head: number; target: number; cursorHeight: number; done: boolean }> { + const result = await this.runOnce(maxHeight); + if (result.cursorHeight < maxHeight && result.processed === 0) { + throw new Error(`confirmed target ${result.target} is below requested to-height ${maxHeight}; cursor is at ${result.cursorHeight}`); + } + return { ...result, done: result.cursorHeight >= maxHeight }; + } + + private async planRange(maxHeight?: number): Promise { + const head = await this.rpc.head(); + const target = Math.max(0, head.height - this.config.confirmationDepth); + const lastHeight = this.config.dryRun + ? Math.max(0, this.config.startHeight - 1) + : await withClient(this.pool!, (client) => getCursor(client, this.config.cursorId, this.config.chainId, this.config.startHeight)); + const { from, to, empty } = nextBlockRange({ lastHeight, confirmedTarget: target, batchSize: this.config.batchSize, maxHeight }); + return { head, target, lastHeight, from, to, empty }; + } + + private fetchRange(from: number, to: number): Promise { + return fetchBlockRange({ + rpc: this.rpc, + from, + to, + concurrency: this.config.indexerMode === "catchup" ? this.config.fetchConcurrency : this.config.realtimeFetchConcurrency, + }); + } + + private normalizeBlock(block: BlockBundle): DecodedBlock { + const events = block.txEvents.flatMap((tx) => + normalizeBlockEvents( + tx.events, + { chainId: this.config.chainId, height: block.height, blockTime: block.time, txHash: tx.txHash }, + { factoryAddress: this.config.factoryAddress, incentivesAddress: this.config.incentivesAddress }, + ), + ); + return { block, events }; + } + + private shouldUseBulkStaging(): boolean { + return !this.config.dryRun + && this.config.indexerMode === "catchup" + && this.config.ingestBulkStagingEnabled + && !this.config.ingestCandlesInline; + } + + private async writeBulkStagingBatch(blocks: DecodedBlock[]): Promise { + if (blocks.length === 0) return 0; + await withClient(this.pool!, async (client) => { + await client.query("BEGIN"); + try { + await stageAndMergeBatch(client, { + batchId: randomUUID(), + chainId: this.config.chainId, + cursorId: this.config.cursorId, + blocks: blocks.map(({ block, events }) => ({ + chainId: this.config.chainId, + height: block.height, + blockHash: block.hash, + parentHash: block.parentHash, + blockTime: block.time, + txCount: block.txCount, + events, + })), + writeCandlesInline: this.config.ingestCandlesInline, + enqueueSnapshots: !this.config.ingestReserveSnapshotsInline, + }); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } + }); + + if (this.config.ingestReserveSnapshotsInline) { + for (const { block, events } of blocks) { + await this.writeReserveSnapshots(events, block.height, block.time); + } + } + return blocks.length; + } + + private async writeBlocksInOrder(blocks: DecodedBlock[]): Promise { + let processed = 0; + for (const { block, events } of blocks) { + if (this.config.dryRun) { + console.log(JSON.stringify({ height: block.height, hash: block.hash, events }, null, 2)); + } else { + await this.writeBlock(block, events); + if (this.config.ingestReserveSnapshotsInline) { + await this.writeReserveSnapshots(events, block.height, block.time); + } + } + processed += 1; + } + return processed; + } + + private async writeBlock(block: BlockBundle, events: NormalizedEvent[]): Promise { + await withClient(this.pool!, async (client) => { + await client.query("BEGIN"); + try { + await recordProcessedBlock(client, { + chainId: this.config.chainId, + height: block.height, + blockHash: block.hash, + parentHash: block.parentHash, + blockTime: block.time, + txCount: block.txCount, + }); + await writeNormalizedEvents(client, this.config.chainId, events, { writeCandlesInline: this.config.ingestCandlesInline }); + if (!this.config.ingestReserveSnapshotsInline) { + await enqueueSnapshotJobs(client, { + chainId: this.config.chainId, + pairAddresses: touchedPairAddresses(events), + height: block.height, + blockTime: block.time, + reason: "touched", + }); + } + await advanceCursor(client, { cursorId: this.config.cursorId, height: block.height, blockHash: block.hash }); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } + }); + } + + private async writeReserveSnapshots(events: NormalizedEvent[], height: number, blockTime: string): Promise { + const touchedPairs = touchedPairAddresses(events); + if (touchedPairs.length === 0) return; + + const knownPairs = await withClient(this.pool!, async (client) => { + const result = await client.query<{ pair_address: string }>( + `SELECT pair_address FROM pools WHERE chain_id = $1 AND pair_address = ANY($2::text[])`, + [this.config.chainId, touchedPairs], + ); + return new Set(result.rows.map((row) => row.pair_address)); + }); + + for (const pairAddress of touchedPairs) { + if (!knownPairs.has(pairAddress)) continue; + try { + const state = await this.rest.poolState(pairAddress, height); + await withClient(this.pool!, async (client) => upsertPoolStateSnapshot(client, { + chainId: this.config.chainId, + pairAddress, + height, + blockTime, + reserves: state.reserves, + totalShare: state.totalShare, + source: "lcd", + })); + } catch (error) { + console.warn("indexer_reserve_snapshot_failed", { pairAddress, height, error: error instanceof Error ? error.message : String(error) }); + } + } + } +} + +function touchedPairAddresses(events: NormalizedEvent[]): string[] { + return Array.from(new Set(events + .filter(isPairStateEvent) + .map((event) => event.pairAddress) + .filter(Boolean))); +} + +function isPairStateEvent(event: NormalizedEvent): event is Extract { + return event.kind === "swap" || event.kind === "provide" || event.kind === "withdraw"; +} + +function countRangeEvents(blocks: DecodedBlock[]): Partial> { + const counts: Partial> = {}; + for (const { events } of blocks) { + for (const event of events) counts[event.kind] = (counts[event.kind] ?? 0) + 1; + } + return counts; +} + +function isReorgHaltError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return /processed block (hash mismatch|parent hash mismatch|conflict)/.test(message); +} + +async function withClient(pool: PgPool, fn: (client: PgClient) => Promise): Promise { + const client = await pool.connect(); + try { + return await fn(client); + } finally { + client.release(); + } +} diff --git a/indexer/src/metrics.ts b/indexer/src/metrics.ts new file mode 100644 index 000000000..cecb6f79f --- /dev/null +++ b/indexer/src/metrics.ts @@ -0,0 +1,85 @@ +export type WriterEventKind = "pool_created" | "swap" | "provide" | "withdraw" | "incentive"; + +export type RangeMetrics = { + from: number; + to: number; + cursor: number; + head: number; + target: number; + blocks: number; + swaps: number; + liquidityEvents: number; + incentiveEvents: number; + durationMs: number; + dbDurationMs: number; +}; + +type RpcErrorCounts = Map; +type WriterEventCounts = Map; + +export class IndexerMetrics { + private readonly startedAtMs = Date.now(); + private fetchBlocksTotal = 0; + private rpcRequestsInFlight = 0; + private readonly rpcErrors: RpcErrorCounts = new Map(); + private decodeBlocksTotal = 0; + private writerBlocksTotal = 0; + private writerCommitSeconds: number | null = null; + private readonly writerEvents: WriterEventCounts = new Map(); + private reorgHalt = 0; + + recordFetchBlock(): void { + this.fetchBlocksTotal += 1; + } + + beginRpcRequest(): void { + this.rpcRequestsInFlight += 1; + } + + endRpcRequest(): void { + this.rpcRequestsInFlight = Math.max(0, this.rpcRequestsInFlight - 1); + } + + recordRpcError(status: string | number): void { + const key = String(status || "unknown"); + this.rpcErrors.set(key, (this.rpcErrors.get(key) ?? 0) + 1); + } + + recordDecodedBlock(): void { + this.decodeBlocksTotal += 1; + } + + recordWriterBlock(commitSeconds: number): void { + this.writerBlocksTotal += 1; + this.writerCommitSeconds = commitSeconds; + } + + recordWriterEvents(counts: Partial>): void { + for (const [kind, value] of Object.entries(counts) as Array<[WriterEventKind, number | undefined]>) { + if (!value) continue; + this.writerEvents.set(kind, (this.writerEvents.get(kind) ?? 0) + value); + } + } + + setReorgHalt(halted: boolean): void { + this.reorgHalt = halted ? 1 : 0; + } + + snapshot() { + const elapsedSeconds = Math.max((Date.now() - this.startedAtMs) / 1000, 0); + const fetchBlocksPerSecond = elapsedSeconds > 0 ? this.fetchBlocksTotal / elapsedSeconds : 0; + return { + fetchBlocksTotal: this.fetchBlocksTotal, + fetchBlocksPerSecond, + rpcRequestsInFlight: this.rpcRequestsInFlight, + rpcErrors: new Map(this.rpcErrors), + decodeBlocksTotal: this.decodeBlocksTotal, + writerBlocksTotal: this.writerBlocksTotal, + writerCommitSeconds: this.writerCommitSeconds, + writerEvents: new Map(this.writerEvents), + reorgHalt: this.reorgHalt, + }; + } +} + +export const indexerMetrics = new IndexerMetrics(); diff --git a/indexer/src/migrate.ts b/indexer/src/migrate.ts new file mode 100644 index 000000000..318efbbbb --- /dev/null +++ b/indexer/src/migrate.ts @@ -0,0 +1,11 @@ +import { loadConfig } from "./config.js"; +import { createPool, runMigrations } from "./db.js"; + +const config = loadConfig(); +const pool = createPool(config); +try { + const applied = await runMigrations(pool); + console.log(`migrations checked: ${applied.join(", ")}`); +} finally { + await pool.end(); +} diff --git a/indexer/src/openapi.ts b/indexer/src/openapi.ts new file mode 100644 index 000000000..586a7c19e --- /dev/null +++ b/indexer/src/openapi.ts @@ -0,0 +1,167 @@ +const errorResponse = { + type: "object", + properties: { + error: { type: "string" }, + message: { type: "string" }, + }, + required: ["error"], +}; + +const pagination = { + type: "object", + properties: { + limit: { type: "integer", minimum: 1, maximum: 500 }, + nextCursor: { type: ["string", "null"] }, + }, + required: ["limit", "nextCursor"], +}; + +const assetAmount = { + type: "object", + properties: { + denom: { type: "string" }, + reserve: { type: ["string", "null"], description: "Pool reserve amount from latest persisted pool_state_snapshots row." }, + amount: { type: "string" }, + valueUsd: { type: ["number", "null"] }, + valueJuno: { type: ["number", "null"] }, + priceUsd: { type: ["number", "null"] }, + priceJuno: { type: ["number", "null"] }, + priceStatus: { type: "string", enum: ["fresh", "stale", "missing"] }, + }, +}; + +const pool = { + type: "object", + properties: { + id: { type: "string" }, + pair: { type: "string" }, + pairAddress: { type: "string" }, + lpToken: { type: ["string", "null"] }, + poolType: { type: ["string", "null"] }, + assets: { type: "array", items: assetAmount }, + totalShare: { type: ["string", "null"], description: "Latest total LP share from persisted pool_state_snapshots." }, + tvlUsd: { type: ["number", "null"], description: "Null when valuation is unavailable; never fabricated as zero." }, + tvlJuno: { type: ["number", "null"] }, + volume24hUsd: { type: ["number", "null"] }, + volume24hJuno: { type: ["number", "null"] }, + volume7dUsd: { type: ["number", "null"] }, + volume7dJuno: { type: ["number", "null"] }, + fees24hUsd: { type: ["number", "null"] }, + fees24hJuno: { type: ["number", "null"] }, + feeBps: { type: ["number", "null"] }, + feeApr: { type: "number" }, + incentivesApr: { type: "number" }, + totalApr: { type: "number" }, + incentivized: { type: "boolean" }, + updatedAt: { type: "string", format: "date-time" }, + dataSource: { type: "string", const: "indexer" }, + isMock: { type: "boolean", const: false }, + }, + required: ["id", "pairAddress", "assets", "dataSource", "isMock"], +}; + +const price = { + type: "object", + properties: { + asset: { type: "string" }, + priceUsd: { type: ["number", "null"] }, + priceJuno: { type: ["number", "null"] }, + source: { type: ["string", "null"] }, + status: { type: "string", enum: ["fresh", "stale", "missing"] }, + stale: { type: "boolean" }, + observedAt: { type: ["string", "null"], format: "date-time" }, + ageMs: { type: ["integer", "null"] }, + isMock: { type: "boolean", const: false }, + }, + required: ["asset", "priceUsd", "priceJuno", "status", "stale", "isMock"], +}; + +const candle = { + type: "object", + properties: { + poolId: { type: "string" }, + pairAddress: { type: "string" }, + baseAsset: { type: "string" }, + quoteAsset: { type: "string" }, + interval: { type: "string", enum: ["5m", "1h", "1d"] }, + bucketStart: { type: "string", format: "date-time" }, + open: { type: ["number", "null"] }, + high: { type: ["number", "null"] }, + low: { type: ["number", "null"] }, + close: { type: ["number", "null"] }, + volume: { type: ["number", "null"] }, + volumeQuote: { type: ["number", "null"], description: "Quote-asset volume, stored separately from USD volume." }, + tradeCount: { type: "integer" }, + dataSource: { type: "string", const: "indexer" }, + isMock: { type: "boolean", const: false }, + }, +}; + +const walletTransaction = { + type: "object", + properties: { + txHash: { type: "string" }, + walletAddress: { type: ["string", "null"] }, + poolId: { type: ["string", "null"] }, + pairAddress: { type: ["string", "null"] }, + type: { type: "string" }, + height: { type: "integer" }, + timestamp: { type: "string", format: "date-time" }, + offerAsset: { type: ["object", "null"] }, + askAsset: { type: ["object", "null"] }, + amountUsd: { type: ["number", "null"] }, + feeUsd: { type: ["number", "null"] }, + success: { type: "boolean" }, + dataSource: { type: "string", const: "indexer" }, + isMock: { type: "boolean", const: false }, + }, +}; + +const limitParam = { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 500 }, required: false }; +const cursorParam = { name: "cursor", in: "query", schema: { type: "string" }, required: false }; +const assetQueryParam = { name: "assets", in: "query", schema: { type: "string" }, required: false, description: "Comma-separated native denoms, IBC denoms, or CW20 contract addresses." }; +const assetPathParam = { name: "asset", in: "path", schema: { type: "string" }, required: true }; +const idPathParam = { name: "id", in: "path", schema: { type: "string" }, required: true, description: "Pool UUID or pair address." }; +const walletPathParam = { name: "addr", in: "path", schema: { type: "string" }, required: true, description: "Juno wallet address." }; +const candleQueryParams = [ + { name: "interval", in: "query", schema: { type: "string", enum: ["5m", "1h", "1d"] }, required: false }, + { name: "from", in: "query", schema: { type: "string", format: "date-time" }, required: false }, + { name: "to", in: "query", schema: { type: "string", format: "date-time" }, required: false }, + { name: "baseAsset", in: "query", schema: { type: "string" }, required: false }, + { name: "quoteAsset", in: "query", schema: { type: "string" }, required: false }, +]; + +function ok(schema: unknown, extra: Record = {}) { + return { + ...extra, + responses: { + "200": { description: "OK", content: { "application/json": { schema } } }, + "400": { description: "Bad request", content: { "application/json": { schema: errorResponse } } }, + "404": { description: "Not found", content: { "application/json": { schema: errorResponse } } }, + "503": { description: "Not ready", content: { "application/json": { schema } } }, + "500": { description: "Internal error", content: { "application/json": { schema: errorResponse } } }, + }, + }; +} + +export const openApiDocument = { + openapi: "3.1.0", + info: { title: "Astroport Juno Production Indexer API", version: "0.1.0" }, + servers: [{ url: "/" }], + paths: { + "/health": { get: ok({ type: "object", properties: { status: { type: "string", const: "ok" }, service: { type: "string" }, chainId: { type: "string" }, confirmationDepth: { type: "number" }, cursorHeight: { type: ["number", "null"] }, cursorAgeMs: { type: ["number", "null"] }, headHeight: { type: ["number", "null"] }, confirmedTargetHeight: { type: ["number", "null"] }, lag: { type: ["number", "null"] }, confirmedLag: { type: ["number", "null"] }, rpcConfigured: { type: "boolean" }, rpcReachable: { type: "boolean" }, dataSource: { type: "string", const: "indexer" }, isMock: { type: "boolean", const: false } } }) }, + "/ready": { get: ok({ type: "object", properties: { status: { type: "string", enum: ["ready", "not_ready"] }, checks: { type: "object", properties: { database: { type: "boolean" }, migrations: { type: "boolean" }, rpc: { type: "boolean" } } }, migrationsApplied: { type: "integer" }, expectedMigrations: { type: ["integer", "null"] }, missingMigrations: { type: "array", items: { type: "string" } }, rpcConfigured: { type: "boolean" }, rpcReachable: { type: "boolean" }, dataSource: { type: "string", const: "indexer" }, isMock: { type: "boolean", const: false } } }) }, + "/openapi.json": { get: ok({ type: "object" }) }, + "/metrics": { get: { responses: { "200": { description: "Prometheus text exposition metrics for indexer readiness, lag, cursor, RPC, and migration status.", content: { "text/plain": { schema: { type: "string" } } } }, "500": { description: "Internal error", content: { "application/json": { schema: errorResponse } } } } } }, + "/stats": { get: ok({ type: "object", properties: { poolCount: { type: "integer" }, tvlUsd: { type: ["number", "null"] }, tvlJuno: { type: ["number", "null"] }, volume24hUsd: { type: ["number", "null"] }, volume24hJuno: { type: ["number", "null"] }, volume7dUsd: { type: ["number", "null"] }, volume7dJuno: { type: ["number", "null"] }, fees24hUsd: { type: ["number", "null"] }, fees24hJuno: { type: ["number", "null"] }, incentivizedPools: { type: "integer" }, updatedAt: { type: "string", format: "date-time" }, dataSource: { type: "string", const: "indexer" }, isMock: { type: "boolean", const: false } } }) }, + "/prices": { get: ok({ type: "object", properties: { data: { type: "array", items: price } }, required: ["data"] }, { parameters: [assetQueryParam] }) }, + "/prices/{asset}": { get: ok(price, { parameters: [assetPathParam] }) }, + "/pools": { get: ok({ type: "object", properties: { data: { type: "array", items: pool }, pagination }, required: ["data", "pagination"] }, { parameters: [limitParam, cursorParam, { name: "pair", in: "query", schema: { type: "string" }, required: false }] }) }, + "/pools/{id}": { get: ok(pool, { parameters: [idPathParam] }) }, + "/pools/{id}/candles": { get: ok({ type: "object", properties: { data: { type: "array", items: candle }, pagination, meta: { type: "object" } }, required: ["data", "pagination", "meta"] }, { parameters: [idPathParam, limitParam, cursorParam, ...candleQueryParams] }) }, + "/pools/{id}/positions": { get: ok({ type: "object", properties: { data: { type: "array" }, pagination }, required: ["data", "pagination"] }, { parameters: [idPathParam, limitParam, cursorParam] }) }, + "/pools/{id}/history": { get: ok({ type: "object", properties: { data: { type: "array", items: walletTransaction }, pagination }, required: ["data", "pagination"] }, { parameters: [idPathParam, limitParam, cursorParam] }) }, + "/wallets/{addr}/positions": { get: ok({ type: "object", properties: { data: { type: "array" }, pagination }, required: ["data", "pagination"] }, { parameters: [walletPathParam, limitParam, cursorParam] }) }, + "/wallets/{addr}/history": { get: ok({ type: "object", properties: { data: { type: "array", items: walletTransaction }, pagination }, required: ["data", "pagination"] }, { parameters: [walletPathParam, limitParam, cursorParam] }) }, + }, +}; diff --git a/indexer/src/ranges.ts b/indexer/src/ranges.ts new file mode 100644 index 000000000..4cd881a62 --- /dev/null +++ b/indexer/src/ranges.ts @@ -0,0 +1,27 @@ +export function parseNonNegativeInteger(value: string, name: string): number { + if (!/^\d+$/.test(value)) throw new Error(`${name} must be a non-negative integer`); + const parsed = Number(value); + if (!Number.isSafeInteger(parsed)) throw new Error(`${name} must be a non-negative integer`); + return parsed; +} + +export type BlockRangeInput = { + lastHeight: number; + confirmedTarget: number; + batchSize: number; + maxHeight?: number; +}; + +export type BlockRange = { + from: number; + to: number; + empty: boolean; +}; + +export function nextBlockRange(input: BlockRangeInput): BlockRange { + const from = input.lastHeight + 1; + const batchTo = input.lastHeight + Math.max(1, input.batchSize); + const cappedTarget = input.maxHeight === undefined ? input.confirmedTarget : Math.min(input.confirmedTarget, input.maxHeight); + const to = Math.min(cappedTarget, batchTo); + return { from, to, empty: to < from }; +} diff --git a/indexer/src/read-model-refresher.ts b/indexer/src/read-model-refresher.ts new file mode 100644 index 000000000..a9976739f --- /dev/null +++ b/indexer/src/read-model-refresher.ts @@ -0,0 +1,44 @@ +import { refreshApiReadModels, type PgPool } from "./db.js"; + +export class ReadModelRefresher { + private timer: NodeJS.Timeout | undefined; + private running = false; + + constructor( + private readonly pool: PgPool, + private readonly options: { chainId: string; intervalMs: number }, + ) {} + + async refreshOnce(): Promise { + if (this.running) return; + this.running = true; + const client = await this.pool.connect(); + try { + const results = await refreshApiReadModels(client, { chainId: this.options.chainId }); + console.log(JSON.stringify({ + msg: "indexer_read_models_refreshed", + role: "indexer", + models: results, + })); + } finally { + client.release(); + this.running = false; + } + } + + start(): void { + if (this.options.intervalMs <= 0 || this.timer) return; + this.timer = setInterval(() => { + this.refreshOnce().catch((error) => { + console.warn("indexer_read_models_refresh_failed", { error: error instanceof Error ? error.message : String(error) }); + }); + }, this.options.intervalMs); + this.timer.unref(); + } + + stop(): void { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = undefined; + } +} diff --git a/indexer/src/refresh-read-models.ts b/indexer/src/refresh-read-models.ts new file mode 100644 index 000000000..db5437b92 --- /dev/null +++ b/indexer/src/refresh-read-models.ts @@ -0,0 +1,16 @@ +import { loadConfig } from "./config.js"; +import { createPool, refreshApiReadModels } from "./db.js"; + +const config = loadConfig(); +const pool = createPool(config); +const client = await pool.connect(); + +try { + const results = await refreshApiReadModels(client, { chainId: config.chainId }); + for (const result of results) { + console.log(`read_model_refreshed model=${result.model} rows=${result.rowsAffected}`); + } +} finally { + client.release(); + await pool.end(); +} diff --git a/indexer/src/rpc.ts b/indexer/src/rpc.ts new file mode 100644 index 000000000..719063bff --- /dev/null +++ b/indexer/src/rpc.ts @@ -0,0 +1,181 @@ +import { createHash } from "node:crypto"; +import type { TendermintEvent } from "./events.js"; +import type { IndexerMetrics } from "./metrics.js"; + +export type ChainHead = { height: number; hash: string }; +export type BlockBundle = { + height: number; + hash: string; + parentHash?: string; + time: string; + txCount: number; + txEvents: Array<{ txHash: string; events: TendermintEvent[] }>; +}; +export type PoolState = { + reserves: Array<{ denom: string; amount: string }>; + totalShare: string | null; +}; + +type Json = Record; + +export class JunoRestClient { + constructor(private readonly restUrl: string, private readonly timeoutMs = 5_000, private readonly maxRetries = 2) {} + + async poolState(pairAddress: string, height?: number): Promise { + const encodedQuery = encodeURIComponent(Buffer.from(JSON.stringify({ pool: {} })).toString("base64")); + const headers: Record = {}; + if (height !== undefined) headers["x-cosmos-block-height"] = String(height); + const path = `/cosmwasm/wasm/v1/contract/${pairAddress}/smart/${encodedQuery}`; + const json = await this.getJson(path, headers); + return normalizePoolState(json.data ?? json); + } + + private async getJson(path: string, headers: Record): Promise { + let lastError: unknown; + for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await fetch(`${this.restUrl}${path}`, { headers, signal: controller.signal }); + if (response.ok) return await response.json() as Json; + if (!isTransientStatus(response.status) || attempt === this.maxRetries) { + throw new Error(`LCD smart query failed: ${response.status} ${response.statusText}`); + } + lastError = new Error(`LCD smart query failed: ${response.status} ${response.statusText}`); + } catch (error) { + lastError = error; + if (attempt === this.maxRetries || !isTransientFetchError(error)) throw error; + } finally { + clearTimeout(timeout); + } + await delay(100 * 2 ** attempt); + } + throw lastError instanceof Error ? lastError : new Error("LCD smart query failed"); + } +} + +export class JunoRpcClient { + private readonly metrics?: IndexerMetrics; + private readonly timeoutMs: number; + private readonly maxRetries: number; + + constructor(private readonly rpcUrl: string, options: { metrics?: IndexerMetrics; timeoutMs?: number; maxRetries?: number } = {}) { + this.metrics = options.metrics; + this.timeoutMs = options.timeoutMs ?? 10_000; + this.maxRetries = options.maxRetries ?? 5; + } + + async head(): Promise { + const json = await this.get("/status") as Json; + const latest = (((json.result as Json).sync_info as Json)); + return { height: Number(latest.latest_block_height), hash: String(latest.latest_block_hash) }; + } + + async block(height: number): Promise { + const [blockJson, resultsJson] = await Promise.all([ + this.get(`/block?height=${height}`) as Promise, + this.get(`/block_results?height=${height}`) as Promise, + ]); + const block = (((blockJson.result as Json).block as Json)); + const header = block.header as Json; + const data = block.data as Json; + const results = resultsJson.result as Json; + const txsResults = (results.txs_results ?? []) as Json[]; + const txs = (data.txs ?? []) as string[]; + const bundle = { + height, + hash: String((blockJson.result as Json).block_id ? ((blockJson.result as Json).block_id as Json).hash : header.last_block_id ?? ""), + parentHash: String((((header.last_block_id as Json | undefined)?.hash) ?? "")) || undefined, + time: String(header.time), + txCount: txs.length, + txEvents: txsResults.map((tx, index) => ({ + txHash: String(tx.hash ?? txHashFromBase64(txs[index]) ?? `height-${height}-tx-${index}`), + events: convertEvents((tx.events ?? []) as Json[]), + })), + }; + this.metrics?.recordFetchBlock(); + return bundle; + } + + private async get(path: string): Promise { + let lastError: unknown; + for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + this.metrics?.beginRpcRequest(); + try { + const response = await fetch(`${this.rpcUrl}${path}`, { signal: controller.signal }); + if (response.ok) return await response.json(); + const error = new Error(`RPC ${path} failed: ${response.status} ${response.statusText}`); + this.metrics?.recordRpcError(response.status); + if (!isTransientStatus(response.status) || attempt === this.maxRetries) throw error; + lastError = error; + } catch (error) { + lastError = error; + if (!(error instanceof Error && error.message.startsWith(`RPC ${path} failed:`))) this.metrics?.recordRpcError("network"); + if (attempt === this.maxRetries || !isTransientFetchError(error)) throw error; + } finally { + clearTimeout(timeout); + this.metrics?.endRpcRequest(); + } + await delay(100 * 2 ** attempt); + } + throw lastError instanceof Error ? lastError : new Error(`RPC ${path} failed`); + } +} + +function isTransientStatus(status: number): boolean { + return status === 408 || status === 425 || status === 429 || status >= 500; +} + +function isTransientFetchError(error: unknown): boolean { + return error instanceof Error && (error.name === "AbortError" || error.name === "TypeError"); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function normalizePoolState(value: unknown): PoolState { + if (!value || typeof value !== "object") throw new Error("LCD pool query returned non-object data"); + const raw = value as Json; + const assets = Array.isArray(raw.assets) ? raw.assets : []; + const reserves = assets.map(normalizePoolAsset).filter((asset): asset is { denom: string; amount: string } => asset !== null); + if (reserves.length === 0) throw new Error("LCD pool query returned no reserves"); + return { reserves, totalShare: raw.total_share === null || raw.total_share === undefined ? null : String(raw.total_share) }; +} + +function normalizePoolAsset(value: unknown): { denom: string; amount: string } | null { + if (!value || typeof value !== "object") return null; + const raw = value as Json; + const denom = normalizeAssetInfo(raw.info ?? raw.asset_info ?? raw.denom ?? raw.asset); + if (!denom || raw.amount === null || raw.amount === undefined) return null; + return { denom, amount: String(raw.amount) }; +} + +function normalizeAssetInfo(value: unknown): string { + if (typeof value === "string") return value; + if (!value || typeof value !== "object") return ""; + const raw = value as Json; + const native = raw.native_token; + if (native && typeof native === "object") return String((native as Json).denom ?? ""); + const token = raw.token; + if (token && typeof token === "object") return String((token as Json).contract_addr ?? ""); + return String(raw.denom ?? raw.asset ?? ""); +} + +function txHashFromBase64(tx?: string): string | undefined { + if (!tx) return undefined; + return createHash("sha256").update(Buffer.from(tx, "base64")).digest("hex").toUpperCase(); +} + +function convertEvents(events: Json[]): TendermintEvent[] { + return events.map((event) => ({ + type: String(event.type), + attributes: ((event.attributes ?? []) as Json[]).map((attribute) => ({ + key: String(attribute.key), + value: String(attribute.value), + index: Boolean(attribute.index), + })), + })); +} diff --git a/indexer/src/seed-asset-metadata.ts b/indexer/src/seed-asset-metadata.ts new file mode 100644 index 000000000..b2ee33990 --- /dev/null +++ b/indexer/src/seed-asset-metadata.ts @@ -0,0 +1,81 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { loadConfig } from "./config.js"; +import { createPool } from "./db.js"; + +type RegistryAsset = { + id?: unknown; + symbol?: unknown; + decimals?: unknown; + logoURI?: unknown; + verified?: unknown; +}; + +type RegistryPool = { + assets?: unknown; +}; + +type Registry = { + chainId?: unknown; + pools?: unknown; +}; + +const args = new Map(); +for (const arg of process.argv.slice(2)) { + const [key, value = ""] = arg.replace(/^--/, "").split("="); + if (key) args.set(key, value); +} + +function isRegistryAsset(value: unknown): value is RegistryAsset { + return typeof value === "object" && value !== null; +} + +function collectAssets(registry: Registry) { + const assets = new Map(); + const pools = Array.isArray(registry.pools) ? registry.pools as RegistryPool[] : []; + for (const pool of pools) { + const poolAssets = Array.isArray(pool.assets) ? pool.assets : []; + for (const asset of poolAssets) { + if (!isRegistryAsset(asset) || typeof asset.id !== "string") continue; + if (!Number.isInteger(asset.decimals) || Number(asset.decimals) < 0 || Number(asset.decimals) > 36) continue; + assets.set(asset.id, { + symbol: typeof asset.symbol === "string" ? asset.symbol : null, + decimals: Number(asset.decimals), + logoUri: typeof asset.logoURI === "string" ? asset.logoURI : null, + verified: asset.verified === true, + }); + } + } + return assets; +} + +const config = loadConfig(); +const registryPath = resolve(args.get("registry") ?? "../../frontend/src/data/registry.juno-1.json"); +const registry = JSON.parse(await readFile(registryPath, "utf8")) as Registry; +const chainId = args.get("chain-id") ?? (typeof registry.chainId === "string" ? registry.chainId : config.chainId); +const assets = collectAssets(registry); +const pool = createPool(config); +const client = await pool.connect(); + +try { + let upserted = 0; + for (const [asset, metadata] of assets) { + const result = await client.query( + `INSERT INTO asset_metadata(chain_id, asset, symbol, decimals, logo_uri, verified, source) + VALUES ($1,$2,$3,$4,$5,$6,'registry') + ON CONFLICT (chain_id, asset) DO UPDATE + SET symbol = EXCLUDED.symbol, + decimals = EXCLUDED.decimals, + logo_uri = EXCLUDED.logo_uri, + verified = EXCLUDED.verified, + source = EXCLUDED.source, + updated_at = now()`, + [chainId, asset, metadata.symbol, metadata.decimals, metadata.logoUri, metadata.verified], + ); + upserted += result.rowCount ?? 0; + } + console.log(`asset_metadata_seeded chain_id=${chainId} assets=${assets.size} rows=${upserted} registry=${registryPath}`); +} finally { + client.release(); + await pool.end(); +} diff --git a/indexer/src/snapshot-worker.ts b/indexer/src/snapshot-worker.ts new file mode 100644 index 000000000..9d4e7ae50 --- /dev/null +++ b/indexer/src/snapshot-worker.ts @@ -0,0 +1,137 @@ +import { loadConfig, type IndexerConfig } from "./config.js"; +import { + claimSnapshotJobs, + createPool, + markSnapshotJobFailed, + markSnapshotJobSucceeded, + upsertPoolStateSnapshot, + type PgClient, + type PgPool, + type SnapshotJob, +} from "./db.js"; +import { JunoRestClient } from "./rpc.js"; + +export type SnapshotWorkerOptions = { + batchSize?: number; + leaseSeconds?: number; + maxAttempts?: number; + pollIntervalMs?: number; + runForever?: boolean; +}; + +const DEFAULT_BATCH_SIZE = 25; +const DEFAULT_LEASE_SECONDS = 60; +const DEFAULT_MAX_ATTEMPTS = 5; + +export class SnapshotWorker { + private readonly pool?: PgPool; + private readonly ownsPool: boolean; + private readonly rest: Pick; + private readonly batchSize: number; + private readonly leaseSeconds: number; + private readonly maxAttempts: number; + private readonly pollIntervalMs: number; + + constructor(private readonly config: IndexerConfig, pool?: PgPool, rest?: Pick, options: SnapshotWorkerOptions = {}) { + this.pool = pool ?? createPool(config); + this.ownsPool = !pool; + this.rest = rest ?? new JunoRestClient(config.restUrl); + this.batchSize = options.batchSize ?? intEnv("SNAPSHOT_WORKER_BATCH_SIZE", DEFAULT_BATCH_SIZE); + this.leaseSeconds = options.leaseSeconds ?? intEnv("SNAPSHOT_JOB_LEASE_SECONDS", DEFAULT_LEASE_SECONDS); + this.maxAttempts = options.maxAttempts ?? intEnv("SNAPSHOT_JOB_MAX_ATTEMPTS", DEFAULT_MAX_ATTEMPTS); + this.pollIntervalMs = options.pollIntervalMs ?? config.pollIntervalMs; + } + + async close(): Promise { + if (this.ownsPool) await this.pool?.end(); + } + + async processBatch(): Promise { + const jobs = await withClient(this.pool!, (client) => claimSnapshotJobs(client, { + chainId: this.config.chainId, + limit: this.batchSize, + leaseSeconds: this.leaseSeconds, + maxAttempts: this.maxAttempts, + })); + for (const job of jobs) await this.processJob(job); + return jobs.length; + } + + async runForever(): Promise { + for (;;) { + const processed = await this.processBatch(); + console.log(`snapshot worker processed=${processed}`); + await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs)); + } + } + + private async processJob(job: SnapshotJob): Promise { + try { + const state = await this.rest.poolState(job.pairAddress, job.height); + await withClient(this.pool!, async (client) => { + await client.query("BEGIN"); + try { + await upsertPoolStateSnapshot(client, { + chainId: job.chainId, + pairAddress: job.pairAddress, + height: job.height, + blockTime: job.blockTime, + reserves: state.reserves, + totalShare: state.totalShare, + source: "lcd", + }); + await markSnapshotJobSucceeded(client, { jobId: job.id, attempt: job.attempts }); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await withClient(this.pool!, (client) => markSnapshotJobFailed(client, { + jobId: job.id, + attempt: job.attempts, + error: message, + permanent: isPermanentSnapshotFailure(error), + maxAttempts: this.maxAttempts, + })); + } + } +} + +export function isPermanentSnapshotFailure(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + const status = message.match(/LCD smart query failed: (\d{3})\b/)?.[1]; + if (status) { + const code = Number(status); + return code >= 400 && code < 500 && ![408, 425, 429].includes(code); + } + return /no reserves|non-object data|unknown pair/i.test(message); +} + +function intEnv(name: string, fallback: number): number { + const value = process.env[name]; + if (!value) return fallback; + const parsed = Number.parseInt(value, 10); + if (!Number.isSafeInteger(parsed) || parsed < 1) throw new Error(`${name} must be an integer greater than or equal to 1`); + return parsed; +} + +async function withClient(pool: PgPool, fn: (client: PgClient) => Promise): Promise { + const client = await pool.connect(); + try { + return await fn(client); + } finally { + client.release(); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const worker = new SnapshotWorker(loadConfig()); + try { + await worker.runForever(); + } finally { + await worker.close(); + } +} diff --git a/indexer/test/api.test.ts b/indexer/test/api.test.ts new file mode 100644 index 000000000..7e9fab52f --- /dev/null +++ b/indexer/test/api.test.ts @@ -0,0 +1,301 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type http from "node:http"; +import { createIndexerApi } from "../src/api.js"; +import { PostgresApiStore } from "../src/api-store.js"; +import { IndexerMetrics } from "../src/metrics.js"; + +type QueryCall = { text: string; values?: unknown[] }; + +class FakeDb { + calls: QueryCall[] = []; + async query(text: string, values?: unknown[]) { + this.calls.push({ text, values }); + if (text === "SELECT 1") return { rows: [{ "?column?": 1 }] }; + if (text.includes("FROM schema_migrations")) return { rows: [{ version: "001_init.sql" }, { version: "002_pool_candles.sql" }, { version: "003_api_pricing_readiness.sql" }] }; + if (text.includes("FROM indexer_cursors")) return { rows: [{ last_height: "42", updated_at: "2026-07-03T00:00:00.000Z" }] }; + if (text.includes("FROM protocol_stats_latest")) return { rows: [{ pool_count: 1, incentivized_pools: 1, updated_at: "2026-07-03T00:00:00.000Z", tvl_usd: null, tvl_juno: "1000", volume_24h_usd: null, volume_24h_juno: "25", volume_7d_usd: null, volume_7d_juno: "100", fees_24h_usd: null, fees_24h_juno: "0.3" }] }; + if (text.includes("FROM token_prices")) return { rows: [{ asset: "ujuno", price_usd: null, price_juno: "1", source: "pool", status: "fresh", observed_at: "2026-07-03T00:00:00.000Z" }] }; + if (text.includes("FROM latest_pool_state") && text.includes("LIMIT 1")) { + return { rows: [poolRow()] }; + } + if (text.includes("FROM latest_pool_state")) return { rows: [poolRow()] }; + if (text.includes("FROM pools p") && text.includes("LIMIT 1")) return { rows: [poolRow()] }; + if (text.includes("FROM pools p")) return { rows: [poolRow()] }; + if (text.includes("FROM pool_candle_buckets")) { + return { rows: [{ pool_id: "pool-1", pair_address: "juno1pair", asset: "ujuno", quote_asset: "uusdc", interval: "1h", bucket_start: "2026-07-03T00:00:00.000Z", open: "1", high: "1.2", low: "0.9", close: "1.1", volume: "10", volume_quote: "11", trade_count: 2 }] }; + } + if (text.includes("FROM wallet_position_latest")) return { rows: [{ wallet_address: "juno1wallet", owner_address: "juno1wallet", pool_id: "pool-1", pair_address: "juno1pair", lp_token_address: "factory/juno1pair/astroport/share", lp_balance: "7", bonded_balance: "2", total_share: "789", tvl_usd: null, tvl_juno: "1000", asset_infos: [{ native_token: { denom: "ujuno" } }, { native_token: { denom: "uusdc" } }], reserves: [{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }], updated_at: "2026-07-03T00:00:00.000Z" }] }; + if (text.includes("FROM wallet_history_flat")) return { rows: [{ tx_hash: "tx-1", wallet_address: "juno1wallet", pair_address: "juno1pair", type: "swap", height: "42", timestamp: "2026-07-03T00:00:00.000Z", offer_asset: { denom: "ujuno", amount: "1" }, ask_asset: { denom: "uusdc", amount: "2" }, amount_usd: null, fee_usd: null, success: true }] }; + throw new Error(`unexpected query: ${text}`); + } +} + +class EmptyReadModelDb { + calls: QueryCall[] = []; + async query(text: string, values?: unknown[]) { + this.calls.push({ text, values }); + if (text === "SELECT 1") return { rows: [{ "?column?": 1 }] }; + if (text.includes("FROM schema_migrations")) return { rows: [{ version: "001_init.sql" }, { version: "002_pool_candles.sql" }, { version: "003_api_pricing_readiness.sql" }] }; + if (text.includes("FROM indexer_cursors")) return { rows: [] }; + if (text.includes("FROM protocol_stats_latest")) return { rows: [] }; + if (text.includes("FROM latest_pool_state")) return { rows: [] }; + if (text.includes("FROM pools p")) return { rows: [] }; + if (text.includes("FROM pool_candle_buckets")) return { rows: [] }; + if (text.includes("FROM wallet_position_latest")) return { rows: [] }; + if (text.includes("FROM wallet_history_flat")) return { rows: [] }; + throw new Error(`unexpected query: ${text}`); + } +} + +class CandleFilterFallbackDb extends FakeDb { + async query(text: string, values?: unknown[]) { + this.calls.push({ text, values }); + if (text.includes("FROM pool_candle_buckets")) { + if (values?.[3] || values?.[4]) return { rows: [] }; + return { rows: [{ pool_id: "pool-1", pair_address: "juno1pair", asset: "ujuno", quote_asset: "uusdc", interval: "5m", bucket_start: "2026-07-03T00:00:00.000Z", open: "1", high: "1.2", low: "0.9", close: "1.1", volume: "10", volume_quote: "11", trade_count: 2 }] }; + } + this.calls.pop(); + return super.query(text, values); + } +} + +function poolRow() { + return { + id: "pool-1", + chain_id: "juno-1", + pair_address: "juno1pair", + liquidity_token_address: "factory/juno1pair/astroport/share", + pool_type: "xyk", + asset_infos: [{ native_token: { denom: "ujuno" } }, { native_token: { denom: "uusdc" } }], + tvl_usd: null, + tvl_juno: "1000", + total_share: "789", + reserves: [{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }], + updated_at: "2026-07-03T00:00:00.000Z", + }; +} + +async function start(db = new FakeDb()) { + const store = new PostgresApiStore(db as never, "juno-1", "cursor"); + const server = createIndexerApi(store); + await new Promise((resolve) => server.listen(0, resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("missing port"); + return { db, server, baseUrl: `http://127.0.0.1:${address.port}` }; +} + +let openServer: http.Server | undefined; +afterEach(async () => { + vi.restoreAllMocks(); + if (openServer) await new Promise((resolve, reject) => openServer!.close((error) => (error ? reject(error) : resolve()))); + openServer = undefined; +}); + +describe("production API", () => { + it("serves health, readiness, stats and OpenAPI without mock markers", async () => { + const { server, baseUrl } = await start(); + openServer = server; + const health = await (await fetch(`${baseUrl}/health`)).json(); + expect(health).toMatchObject({ status: "ok", service: "astroport-juno-indexer", dataSource: "indexer", isMock: false, confirmationDepth: 0, cursorHeight: 42, confirmedTargetHeight: null, confirmedLag: null, rpcConfigured: false, rpcReachable: false }); + const ready = await (await fetch(`${baseUrl}/ready`)).json(); + expect(ready).toMatchObject({ status: "ready", database: "ok", migrationsApplied: 3, checks: { database: true, migrations: true, rpc: true } }); + const stats = await (await fetch(`${baseUrl}/stats`)).json(); + expect(stats).toMatchObject({ poolCount: 1, tvlUsd: null, tvlJuno: 1000, volume24hUsd: null, volume24hJuno: 25, incentivizedPools: 1, isMock: false }); + const openapi = await (await fetch(`${baseUrl}/openapi.json`)).json(); + expect(openapi.paths["/ready"]).toBeTruthy(); + expect(openapi.paths["/metrics"]).toBeTruthy(); + }); + + it("serves Prometheus metrics for readiness, cursor, RPC, and migrations", async () => { + const { server, baseUrl } = await start(); + openServer = server; + + const response = await fetch(`${baseUrl}/metrics`); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/plain"); + expect(response.headers.get("cache-control")).toContain("no-store"); + const body = await response.text(); + expect(body).toContain("# HELP juno_indexer_ready"); + expect(body).toContain('juno_indexer_ready{chain_id="juno-1"} 1'); + expect(body).toContain('juno_indexer_rpc_configured{chain_id="juno-1"} 0'); + expect(body).toContain('juno_indexer_rpc_reachable{chain_id="juno-1"} 0'); + expect(body).toContain('juno_indexer_cursor_height{chain_id="juno-1"} 42'); + expect(body).toContain('juno_indexer_cursor_age_ms{chain_id="juno-1"}'); + expect(body).toContain('juno_indexer_migrations_applied{chain_id="juno-1"} 3'); + }); + + it("serves in-process ingestion throughput metrics when a collector is attached", async () => { + const store = new PostgresApiStore(new FakeDb() as never, "juno-1", "cursor"); + const metrics = new IndexerMetrics(); + metrics.recordFetchBlock(); + metrics.recordFetchBlock(); + metrics.beginRpcRequest(); + metrics.recordRpcError(429); + metrics.recordDecodedBlock(); + metrics.recordWriterBlock(0.125); + metrics.recordWriterEvents({ swap: 2, provide: 1, incentive: 1 }); + metrics.setReorgHalt(true); + const server = createIndexerApi(store, metrics); + await new Promise((resolve) => server.listen(0, resolve)); + openServer = server; + const address = server.address(); + if (!address || typeof address === "string") throw new Error("missing port"); + + const body = await (await fetch(`http://127.0.0.1:${address.port}/metrics`)).text(); + + expect(body).toContain("# HELP juno_indexer_fetch_blocks_total"); + expect(body).toContain("juno_indexer_fetch_blocks_total 2"); + expect(body).toMatch(/juno_indexer_fetch_blocks_per_second \d/); + expect(body).toContain("juno_indexer_fetch_rpc_requests_in_flight 1"); + expect(body).toContain('juno_indexer_fetch_rpc_error_total{status="429"} 1'); + expect(body).toContain("juno_indexer_decode_blocks_total 1"); + expect(body).toContain("juno_indexer_writer_blocks_total 1"); + expect(body).toContain("juno_indexer_writer_commit_seconds 0.125"); + expect(body).toContain('juno_indexer_writer_events_total{kind="swap"} 2'); + expect(body).toContain('juno_indexer_writer_events_total{kind="provide"} 1'); + expect(body).toContain('juno_indexer_writer_events_total{kind="incentive"} 1'); + expect(body).toContain("juno_indexer_reorg_halt 1"); + }); + + it("uses one shared RPC head check per metrics scrape", async () => { + const originalFetch = globalThis.fetch.bind(globalThis); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + if (String(input) === "http://rpc.example/status") { + return { + ok: true, + json: async () => ({ result: { sync_info: { latest_block_height: "50", latest_block_hash: "head-hash" } } }), + } as Response; + } + return originalFetch(input, init); + }); + const store = new PostgresApiStore(new FakeDb() as never, "juno-1", "cursor", { rpcUrl: "http://rpc.example", confirmationDepth: 2 }); + const server = createIndexerApi(store); + await new Promise((resolve) => server.listen(0, resolve)); + openServer = server; + const address = server.address(); + if (!address || typeof address === "string") throw new Error("missing port"); + + const body = await (await fetch(`http://127.0.0.1:${address.port}/metrics`)).text(); + + const rpcCalls = fetchSpy.mock.calls.filter(([input]) => String(input) === "http://rpc.example/status"); + expect(rpcCalls).toHaveLength(1); + expect(rpcCalls[0]?.[0]).toBe("http://rpc.example/status"); + expect(body).toContain('juno_indexer_rpc_configured{chain_id="juno-1"} 1'); + expect(body).toContain('juno_indexer_rpc_reachable{chain_id="juno-1"} 1'); + expect(body).toContain('juno_indexer_head_height{chain_id="juno-1"} 50'); + expect(body).toContain('juno_indexer_confirmed_target_height{chain_id="juno-1"} 48'); + expect(body).toContain('juno_indexer_confirmed_lag_blocks{chain_id="juno-1"} 6'); + }); + + it("returns frontend-compatible pool, price and candle responses from Postgres rows", async () => { + const { db, server, baseUrl } = await start(); + openServer = server; + const pools = await (await fetch(`${baseUrl}/pools`)).json(); + expect(pools.data[0]).toMatchObject({ id: "pool-1", pairAddress: "juno1pair", tvlUsd: null, tvlJuno: 1000, totalShare: "789", isMock: false }); + expect(pools.data[0].assets[0]).toMatchObject({ denom: "ujuno", reserve: "123", priceJuno: null, priceStatus: "missing" }); + expect(pools.data[0].assets[1]).toMatchObject({ denom: "uusdc", reserve: "456" }); + + const poolDetail = await (await fetch(`${baseUrl}/pools/juno1pair`)).json(); + expect(poolDetail).toMatchObject({ id: "pool-1", pairAddress: "juno1pair", totalShare: "789", isMock: false }); + expect(poolDetail.assets[0]).toMatchObject({ denom: "ujuno", reserve: "123" }); + expect(poolDetail.assets[1]).toMatchObject({ denom: "uusdc", reserve: "456" }); + + const price = await (await fetch(`${baseUrl}/prices/ujuno`)).json(); + expect(price).toMatchObject({ asset: "ujuno", priceUsd: null, priceJuno: 1, source: "pool", status: "fresh", isMock: false }); + + const candles = await (await fetch(`${baseUrl}/pools/juno1pair/candles?interval=1h&limit=999`)).json(); + expect(candles.pagination.limit).toBe(500); + expect(candles.meta).toMatchObject({ pairAddress: "juno1pair", dataSource: "indexer", isMock: false }); + expect(candles.data[0]).toMatchObject({ baseAsset: "ujuno", quoteAsset: "uusdc", close: 1.1, volumeQuote: 11 }); + expect(db.calls.some((call) => call.text.includes("FROM latest_pool_state"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM pool_candle_buckets"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM token_candles"))).toBe(false); + }); + + it("falls back to canonical pair candles when requested asset filters do not match", async () => { + const { db, server, baseUrl } = await start(new CandleFilterFallbackDb()); + openServer = server; + + const candles = await (await fetch(`${baseUrl}/pools/juno1pair/candles?interval=5m&baseAsset=uusdc"eAsset=ujuno&limit=20`)).json(); + + expect(candles.data[0]).toMatchObject({ baseAsset: "ujuno", quoteAsset: "uusdc", close: 1.1 }); + expect(candles.meta).toMatchObject({ + pairAddress: "juno1pair", + interval: "5m", + baseAsset: null, + quoteAsset: null, + requestedBaseAsset: "uusdc", + requestedQuoteAsset: "ujuno", + filterFallback: true, + }); + expect(db.calls.filter((call) => call.text.includes("FROM pool_candle_buckets"))).toHaveLength(2); + }); + + it("serves wallet history and positions from read models", async () => { + const { db, server, baseUrl } = await start(); + openServer = server; + + const history = await (await fetch(`${baseUrl}/wallets/juno1wallet/history`)).json(); + expect(history.data[0]).toMatchObject({ txHash: "tx-1", walletAddress: "juno1wallet", pairAddress: "juno1pair", type: "swap", height: 42, isMock: false }); + expect(history.data[0].offerAsset).toEqual({ denom: "ujuno", amount: "1" }); + + const poolHistory = await (await fetch(`${baseUrl}/pools/juno1pair/history?limit=10`)).json(); + expect(poolHistory.data[0]).toMatchObject({ txHash: "tx-1", pairAddress: "juno1pair", type: "swap" }); + expect(poolHistory.pagination.limit).toBe(10); + + const positions = await (await fetch(`${baseUrl}/wallets/juno1wallet/positions`)).json(); + expect(positions.data[0]).toMatchObject({ walletAddress: "juno1wallet", poolId: "pool-1", pairAddress: "juno1pair", lpBalance: "7", bondedBalance: "2", shareBps: 114, valueUsd: null }); + expect(positions.data[0].valueJuno).toBeCloseTo((9 / 789) * 1000, 6); + expect(positions.data[0].assets[0]).toMatchObject({ denom: "ujuno", reserve: "123", amount: "1" }); + expect(db.calls.some((call) => call.text.includes("FROM wallet_history_flat"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM wallet_position_latest"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM swaps"))).toBe(false); + expect(db.calls.some((call) => call.text.includes("FROM positions"))).toBe(false); + }); + + it("returns honest empty API responses when read models have no production rows", async () => { + const { db, server, baseUrl } = await start(new EmptyReadModelDb() as never); + openServer = server; + + const stats = await (await fetch(`${baseUrl}/stats`)).json(); + expect(stats).toMatchObject({ poolCount: 0, tvlUsd: null, tvlJuno: null, incentivizedPools: 0, isMock: false }); + + const pools = await (await fetch(`${baseUrl}/pools`)).json(); + expect(pools).toMatchObject({ data: [], pagination: { limit: 50, nextCursor: null } }); + + const history = await (await fetch(`${baseUrl}/wallets/juno1empty/history`)).json(); + expect(history).toMatchObject({ data: [], pagination: { limit: 50, nextCursor: null } }); + + const positions = await (await fetch(`${baseUrl}/wallets/juno1empty/positions`)).json(); + expect(positions).toMatchObject({ data: [], pagination: { limit: 50, nextCursor: null } }); + + const poolDetail = await fetch(`${baseUrl}/pools/juno1missing`); + expect(poolDetail.status).toBe(404); + expect(db.calls.some((call) => call.text.includes("FROM protocol_stats_latest"))).toBe(true); + expect(db.calls.some((call) => call.text.includes("FROM latest_pool_state"))).toBe(true); + }); + + it("returns HTTP 503 when readiness checks report not_ready", async () => { + const db = new FakeDb(); + const store = new PostgresApiStore(db as never, "juno-1", "cursor", { expectedMigrationVersions: ["001_init.sql", "002_pool_candles.sql", "003_api_pricing_readiness.sql", "004_pool_state_source_precedence.sql"] }); + const server = createIndexerApi(store); + await new Promise((resolve) => server.listen(0, resolve)); + openServer = server; + const address = server.address(); + if (!address || typeof address === "string") throw new Error("missing port"); + + const response = await fetch(`http://127.0.0.1:${address.port}/ready`); + expect(response.status).toBe(503); + await expect(response.json()).resolves.toMatchObject({ status: "not_ready", checks: { migrations: false }, missingMigrations: ["004_pool_state_source_precedence.sql"] }); + }); + + it("returns structured errors without leaking database internals", async () => { + const db = { query: async () => { throw new Error("secret database internals"); } }; + const { server, baseUrl } = await start(db as never); + openServer = server; + const response = await fetch(`${baseUrl}/stats`); + expect(response.status).toBe(500); + const body = await response.json(); + expect(body).toEqual({ error: "internal_error" }); + }); +}); diff --git a/indexer/test/block-fetcher.test.ts b/indexer/test/block-fetcher.test.ts new file mode 100644 index 000000000..a99dfaa20 --- /dev/null +++ b/indexer/test/block-fetcher.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import { fetchBlockRange } from "../src/block-fetcher.js"; +import type { BlockBundle, JunoRpcClient } from "../src/rpc.js"; + +function bundle(height: number): BlockBundle { + return { height, hash: `hash-${height}`, time: "2026-01-01T00:00:00Z", txCount: 0, txEvents: [] }; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe("fetchBlockRange", () => { + it("caps in-flight block fetches at the requested concurrency", async () => { + let active = 0; + let maxActive = 0; + const rpc = { + async block(height: number): Promise { + active += 1; + maxActive = Math.max(maxActive, active); + await sleep(5); + active -= 1; + return bundle(height); + }, + } as unknown as JunoRpcClient; + + const blocks = await fetchBlockRange({ rpc, from: 10, to: 16, concurrency: 3 }); + + expect(maxActive).toBe(3); + expect(blocks.map((block) => block.height)).toEqual([10, 11, 12, 13, 14, 15, 16]); + }); + + it("returns bundles sorted by ascending height when requests resolve out of order", async () => { + const rpc = { + async block(height: number): Promise { + await sleep((5 - height) * 5); + return bundle(height); + }, + } as unknown as JunoRpcClient; + + const blocks = await fetchBlockRange({ rpc, from: 1, to: 4, concurrency: 4 }); + + expect(blocks.map((block) => block.height)).toEqual([1, 2, 3, 4]); + }); + + it("fails the whole range with the exhausted height when one block fetch fails", async () => { + const rpc = { + async block(height: number): Promise { + if (height === 3) throw new Error("RPC /block?height=3 failed: 503 Service Unavailable"); + return bundle(height); + }, + } as unknown as JunoRpcClient; + + await expect(fetchBlockRange({ rpc, from: 1, to: 5, concurrency: 2 })).rejects.toThrow( + /failed to fetch block 3: RPC \/block\?height=3 failed: 503 Service Unavailable/, + ); + }); + + it("stops scheduling new heights after a worker fails", async () => { + const calls: number[] = []; + const rpc = { + async block(height: number): Promise { + calls.push(height); + if (height === 1) { + await sleep(20); + return bundle(height); + } + if (height === 2) throw new Error("boom"); + return bundle(height); + }, + } as unknown as JunoRpcClient; + + await expect(fetchBlockRange({ rpc, from: 1, to: 5, concurrency: 2 })).rejects.toThrow(/failed to fetch block 2: boom/); + expect(calls).toEqual([1, 2]); + }); + + it("rejects invalid concurrency clearly", async () => { + const rpc = { block: async (height: number) => bundle(height) } as unknown as JunoRpcClient; + + await expect(fetchBlockRange({ rpc, from: 1, to: 1, concurrency: 0 })).rejects.toThrow(/concurrency/i); + }); +}); diff --git a/indexer/test/candles.test.ts b/indexer/test/candles.test.ts new file mode 100644 index 000000000..2787352ff --- /dev/null +++ b/indexer/test/candles.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { aggregateSwapsToCandles, bucketStartFor, deriveCanonicalSwapPrice } from "../src/candles.js"; + +describe("candle helpers", () => { + it("buckets timestamps for supported intervals", () => { + expect(bucketStartFor("2026-07-02T12:34:56.000Z", "5m")).toBe("2026-07-02T12:30:00.000Z"); + expect(bucketStartFor("2026-07-02T12:34:56.000Z", "1h")).toBe("2026-07-02T12:00:00.000Z"); + expect(bucketStartFor("2026-07-02T12:34:56.000Z", "1d")).toBe("2026-07-02T00:00:00.000Z"); + }); + + it("derives a deterministic decimals-aware price regardless of swap direction", () => { + expect(deriveCanonicalSwapPrice({ pairAddress: "juno1pair", blockTime: "2026-07-02T12:00:00Z", offerAsset: "ujuno", offerAmount: "1000000", askAsset: "uusdc", returnAmount: "1250000" }, { ujuno: 6, uusdc: 6 })).toMatchObject({ + baseAsset: "ujuno", + quoteAsset: "uusdc", + price: "1.25", + volume: "1", + volumeQuote: "1.25", + }); + expect(deriveCanonicalSwapPrice({ pairAddress: "juno1pair", blockTime: "2026-07-02T12:01:00Z", offerAsset: "uusdc", offerAmount: "2500000", askAsset: "ujuno", returnAmount: "2000000" }, { ujuno: 6, uusdc: 6 })).toMatchObject({ + baseAsset: "ujuno", + quoteAsset: "uusdc", + price: "1.25", + volume: "2", + volumeQuote: "2.5", + }); + }); + + it("aggregates swaps into OHLC candles", () => { + const candles = aggregateSwapsToCandles([ + { pairAddress: "juno1pair", blockTime: "2026-07-02T12:01:00Z", offerAsset: "ujuno", offerAmount: "1000000", askAsset: "uusdc", returnAmount: "1000000" }, + { pairAddress: "juno1pair", blockTime: "2026-07-02T12:10:00Z", offerAsset: "ujuno", offerAmount: "1000000", askAsset: "uusdc", returnAmount: "1200000" }, + { pairAddress: "juno1pair", blockTime: "2026-07-02T12:20:00Z", offerAsset: "uusdc", offerAmount: "900000", askAsset: "ujuno", returnAmount: "1000000" }, + ], "1h", { ujuno: 6, uusdc: 6 }); + + expect(candles).toHaveLength(1); + expect(candles[0]).toMatchObject({ + bucketStart: "2026-07-02T12:00:00.000Z", + open: "1", + high: "1.19999999999999996", + low: "0.900000000000000022", + close: "0.900000000000000022", + tradeCount: 3, + volume: "3", + }); + }); +}); diff --git a/indexer/test/config.test.ts b/indexer/test/config.test.ts new file mode 100644 index 000000000..ea036d5c0 --- /dev/null +++ b/indexer/test/config.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_START_HEIGHT, loadConfig } from "../src/config.js"; + +const performanceEnvNames = [ + "INDEXER_MODE", + "RANGE_SIZE", + "FETCH_WINDOW_SIZE", + "FETCH_CONCURRENCY", + "REALTIME_FETCH_CONCURRENCY", + "RPC_TIMEOUT_MS", + "RPC_MAX_RETRIES", + "INGEST_CANDLES_INLINE", + "INGEST_RESERVE_SNAPSHOTS_INLINE", + "INGEST_AGGREGATES_INLINE", + "INGEST_BULK_STAGING_ENABLED", + "READ_MODEL_REFRESH_INTERVAL_MS", +] as const; + +function withEnv(overrides: Record, run: () => void): void { + const previous = { ...process.env }; + try { + for (const name of ["DATABASE_URL", "START_HEIGHT", ...performanceEnvNames]) delete process.env[name]; + for (const [name, value] of Object.entries(overrides)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + run(); + } finally { + process.env = previous; + } +} + +describe("config", () => { + it("loads sane defaults", () => { + withEnv({}, () => { + const config = loadConfig(); + expect(config.chainId).toBe("juno-1"); + expect(config.databaseUrl).toBe("postgres://postgres:postgres@localhost:5432/astroport_indexer"); + expect(config.startHeight).toBe(DEFAULT_START_HEIGHT); + expect(config.batchSize).toBeGreaterThan(0); + expect(config.wsUrl).toContain("websocket"); + expect(config.indexerMode).toBe("realtime"); + expect(config.rangeSize).toBe(5_000); + expect(config.fetchWindowSize).toBe(250); + expect(config.fetchConcurrency).toBe(32); + expect(config.realtimeFetchConcurrency).toBe(8); + expect(config.rpcTimeoutMs).toBe(10_000); + expect(config.rpcMaxRetries).toBe(5); + expect(config.ingestCandlesInline).toBe(true); + expect(config.ingestReserveSnapshotsInline).toBe(true); + expect(config.ingestAggregatesInline).toBe(false); + expect(config.ingestBulkStagingEnabled).toBe(false); + expect(config.readModelRefreshIntervalMs).toBe(15_000); + expect(config.priceProviderName).toBe("provider"); + expect(config.priceCacheTtlMs).toBe(300_000); + expect(config.priceAllowStale).toBe(true); + expect(config.apiPort).toBe(8787); + }); + }); + + it("loads performance runtime overrides", () => { + withEnv({ + INDEXER_MODE: "catchup", + RANGE_SIZE: "10000", + FETCH_WINDOW_SIZE: "500", + FETCH_CONCURRENCY: "64", + REALTIME_FETCH_CONCURRENCY: "4", + RPC_TIMEOUT_MS: "20000", + RPC_MAX_RETRIES: "7", + INGEST_CANDLES_INLINE: "false", + INGEST_RESERVE_SNAPSHOTS_INLINE: "0", + INGEST_AGGREGATES_INLINE: "true", + INGEST_BULK_STAGING_ENABLED: "yes", + READ_MODEL_REFRESH_INTERVAL_MS: "0", + }, () => { + expect(loadConfig()).toMatchObject({ + indexerMode: "catchup", + rangeSize: 10_000, + fetchWindowSize: 500, + fetchConcurrency: 64, + realtimeFetchConcurrency: 4, + rpcTimeoutMs: 20_000, + rpcMaxRetries: 7, + ingestCandlesInline: false, + ingestReserveSnapshotsInline: false, + ingestAggregatesInline: true, + ingestBulkStagingEnabled: true, + readModelRefreshIntervalMs: 0, + }); + }); + }); + + it("validates integer values", () => { + withEnv({ START_HEIGHT: "not-a-number" }, () => { + expect(() => loadConfig()).toThrow(/START_HEIGHT/); + }); + }); + + it("validates indexer mode", () => { + withEnv({ INDEXER_MODE: "fast" }, () => { + expect(() => loadConfig()).toThrow(/INDEXER_MODE must be either "realtime" or "catchup"/); + }); + }); + + it("requires concurrency and window sizes to be at least one", () => { + withEnv({ FETCH_WINDOW_SIZE: "0" }, () => { + expect(() => loadConfig()).toThrow(/FETCH_WINDOW_SIZE must be an integer greater than or equal to 1/); + }); + withEnv({ FETCH_CONCURRENCY: "0" }, () => { + expect(() => loadConfig()).toThrow(/FETCH_CONCURRENCY must be an integer greater than or equal to 1/); + }); + withEnv({ REALTIME_FETCH_CONCURRENCY: "0" }, () => { + expect(() => loadConfig()).toThrow(/REALTIME_FETCH_CONCURRENCY must be an integer greater than or equal to 1/); + }); + }); + + it("requires fetch concurrency to fit within the fetch window", () => { + withEnv({ FETCH_WINDOW_SIZE: "10", FETCH_CONCURRENCY: "11" }, () => { + expect(() => loadConfig()).toThrow(/FETCH_CONCURRENCY must be less than or equal to FETCH_WINDOW_SIZE/); + }); + }); + + it("allows non-negative retry and timeout values", () => { + withEnv({ RPC_TIMEOUT_MS: "0", RPC_MAX_RETRIES: "0" }, () => { + expect(loadConfig()).toMatchObject({ rpcTimeoutMs: 0, rpcMaxRetries: 0 }); + }); + }); +}); diff --git a/indexer/test/db.test.ts b/indexer/test/db.test.ts new file mode 100644 index 000000000..ddfbf0b23 --- /dev/null +++ b/indexer/test/db.test.ts @@ -0,0 +1,593 @@ +import { mkdtemp, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { describe, expect, it } from "vitest"; +import { backfillTokenCandles, claimSnapshotJobs, enqueueSnapshotJobs, listMigrationFiles, markSnapshotJobFailed, markSnapshotJobSucceeded, processNextCandleJob, recordProcessedBlock, refreshApiReadModels, runMigrations, stageAndMergeBatch, upsertPoolStateSnapshot, writeNormalizedEvent, writeNormalizedEvents } from "../src/db.js"; + +type Query = { text: string; values?: unknown[] }; + +class FakeMigrationPool { + queries: Query[] = []; + applied = new Set(); + + async query>(text: string, values?: unknown[]): Promise<{ rows: T[] }> { + this.queries.push({ text, values }); + if (text === "SELECT version FROM schema_migrations") { + return { rows: [...this.applied].map((version) => ({ version }) as T) }; + } + if (text.startsWith("INSERT INTO schema_migrations")) { + this.applied.add(String(values?.[0])); + return { rows: [] }; + } + return { rows: [] }; + } +} + +class FakeBlockClient { + rowsByKey = new Map(); + queries: Query[] = []; + nextWriteRowCount = 1; + + async query>(text: string, values?: unknown[]): Promise<{ rows: T[]; rowCount: number }> { + this.queries.push({ text, values }); + if (text.includes("FROM processed_blocks") && text.includes("height = $2 - 1")) { + const rows = (this.rowsByKey.get(`previous:${String(values?.[1])}`) ?? []) as T[]; + return { rows, rowCount: rows.length }; + } + if (text.includes("FROM processed_blocks") && text.includes("height = $2")) { + const rows = (this.rowsByKey.get(`existing:${String(values?.[1])}`) ?? []) as T[]; + return { rows, rowCount: rows.length }; + } + if (text.includes("INSERT INTO processed_blocks")) return { rows: [], rowCount: this.nextWriteRowCount }; + if (text.includes("INSERT INTO pool_state_snapshots")) return { rows: [], rowCount: 1 }; + if (text.includes("INSERT INTO snapshot_jobs")) return { rows: [], rowCount: 1 }; + if (text.includes("WITH claimable")) return { rows: [{ id: "7", chain_id: "juno-1", pair_address: "juno1pair", height: "39381355", block_time: "2026-07-01T03:01:00Z", reason: "touched", status: "leased", attempts: 1 }] as T[], rowCount: 1 }; + if (text.includes("UPDATE snapshot_jobs")) return { rows: [], rowCount: 1 }; + if (text.includes("FROM pools") && text.includes("pair_address")) { + const rows = (this.rowsByKey.get(`pool:${String(values?.[1])}`) ?? []) as T[]; + return { rows, rowCount: rows.length }; + } + return { rows: [], rowCount: 1 }; + } +} + +class FakeCandleClient { + queries: Query[] = []; + + constructor( + private readonly metadataRows: Array<{ asset: string; decimals: number | string | null }> = [{ asset: "ujuno", decimals: 6 }, { asset: "factory/token18", decimals: 18 }], + private readonly swapRow: Record = { pair_address: "juno1pair", block_time: "2026-07-01T03:01:00Z", offer_asset: "factory/backfill18", offer_amount: "2000000000000000000", ask_asset: "ujuno-backfill", return_amount: "3000000", height: "39381355", tx_hash: "tx", msg_index: "0", event_index: "0" }, + private poolRows: Array<{ id: string; pair_address?: string }> = [{ id: "pool-1", pair_address: "juno1pair" }], + ) {} + + async query>(text: string, values?: unknown[]): Promise<{ rows: T[]; rowCount: number }> { + this.queries.push({ text, values }); + if (text.includes("INSERT INTO pools")) { + this.poolRows = [{ id: "pool-created", pair_address: String(values?.[1]) }, ...this.poolRows]; + return { rows: [], rowCount: 1 }; + } + if (text.includes("INSERT INTO swaps")) return { rows: [{ id: "swap-1", pool_id: values?.[1] ?? null }] as T[], rowCount: 1 }; + if (text.includes("INSERT INTO candle_jobs")) return { rows: [], rowCount: 1 }; + if (text.includes("WITH next_job")) return { rows: [{ id: "job-1", chain_id: "juno-1", pair_address: "juno1pair", from_time: "2026-07-01T00:00:00.000Z", to_time: "2026-07-02T00:00:00.000Z", attempts: 1, worker_id: values?.[1] }] as T[], rowCount: 1 }; + if (text.includes("UPDATE candle_jobs")) return { rows: [], rowCount: 1 }; + if (text.includes("FROM swaps")) return { rows: [this.swapRow] as T[], rowCount: 1 }; + if (text.includes("FROM asset_metadata")) { + const requested = new Set((values?.[1] as string[]) ?? []); + const rows = this.metadataRows.filter((row) => requested.has(row.asset)); + return { rows: rows as T[], rowCount: rows.length }; + } + if (text.includes("FROM pools") && text.includes("pair_address")) { + const pairAddress = String(values?.[1]); + const rows = this.poolRows.filter((row) => row.pair_address === pairAddress || row.pair_address === undefined); + return { rows: rows as T[], rowCount: rows.length }; + } + if (text.includes("INSERT INTO token_candles")) return { rows: [], rowCount: 1 }; + return { rows: [], rowCount: 0 }; + } +} + +class FakeStageClient { + queries: Query[] = []; + processedBlockRowCount = 1; + previousBlockHash?: string; + + async query>(text: string, values?: unknown[]): Promise<{ rows: T[]; rowCount: number }> { + this.queries.push({ text, values }); + if (text.includes("FROM processed_blocks") && text.includes("height = $2")) { + const rows = this.previousBlockHash ? [{ block_hash: this.previousBlockHash }] as T[] : []; + return { rows, rowCount: rows.length }; + } + if (text.includes("INSERT INTO processed_blocks")) return { rows: [], rowCount: this.processedBlockRowCount }; + return { rows: [], rowCount: 1 }; + } +} + +describe("migration runner", () => { + it("lists repository migrations from the default runtime path", async () => { + await expect(listMigrationFiles()).resolves.toEqual([ + "001_init.sql", + "002_pool_candles.sql", + "003_api_pricing_readiness.sql", + "004_pool_state_source_precedence.sql", + "005_snapshot_jobs.sql", + "006_candle_jobs.sql", + "007_bulk_staging.sql", + "008_read_models.sql", + "009_juno_stats_derivation.sql", + ]); + }); + + it("lists only SQL migrations in deterministic order", async () => { + const dir = await mkdtemp(join(tmpdir(), "juno-indexer-migration-list-")); + await writeFile(join(dir, "002_next.sql"), "SELECT 2;"); + await writeFile(join(dir, "README.md"), "not a migration"); + await writeFile(join(dir, "001_init.sql"), "SELECT 1;"); + + await expect(listMigrationFiles(dir)).resolves.toEqual(["001_init.sql", "002_next.sql"]); + }); + + it("records migrations once and skips already-applied files on subsequent runs", async () => { + const dir = await mkdtemp(join(tmpdir(), "juno-indexer-migrations-")); + await writeFile(join(dir, "001_init.sql"), "SELECT 1;"); + await writeFile(join(dir, "002_next.sql"), "SELECT 2;"); + const pool = new FakeMigrationPool(); + + await expect(runMigrations(pool as never, dir)).resolves.toEqual(["001_init.sql", "002_next.sql"]); + const firstRunSqlExecutions = pool.queries.filter((query) => query.text === "SELECT 1;" || query.text === "SELECT 2;"); + expect(firstRunSqlExecutions).toHaveLength(2); + + pool.queries = []; + await expect(runMigrations(pool as never, dir)).resolves.toEqual([]); + const secondRunSqlExecutions = pool.queries.filter((query) => query.text === "SELECT 1;" || query.text === "SELECT 2;"); + expect(secondRunSqlExecutions).toHaveLength(0); + }); +}); + +describe("processed block recording", () => { + it("rejects a conflicting block hash for an already processed height", async () => { + const client = new FakeBlockClient(); + client.rowsByKey.set("existing:39381305", [{ block_hash: "old-hash", parent_hash: "parent" }]); + + await expect(recordProcessedBlock(client as never, { + chainId: "juno-1", + height: 39381305, + blockHash: "new-hash", + parentHash: "parent", + blockTime: "2026-07-01T03:00:00Z", + txCount: 1, + })).rejects.toThrow(/block hash mismatch/i); + }); + + it("rejects a parent-hash mismatch against the previous processed block", async () => { + const client = new FakeBlockClient(); + client.rowsByKey.set("previous:39381306", [{ block_hash: "expected-parent" }]); + + await expect(recordProcessedBlock(client as never, { + chainId: "juno-1", + height: 39381306, + blockHash: "child-hash", + parentHash: "different-parent", + blockTime: "2026-07-01T03:00:06Z", + txCount: 1, + })).rejects.toThrow(/parent hash mismatch/i); + }); + + it("rejects an atomic write conflict when the guarded upsert affects no rows", async () => { + const client = new FakeBlockClient(); + client.nextWriteRowCount = 0; + + await expect(recordProcessedBlock(client as never, { + chainId: "juno-1", + height: 39381307, + blockHash: "late-conflict-hash", + parentHash: "parent", + blockTime: "2026-07-01T03:00:12Z", + txCount: 1, + })).rejects.toThrow(/processed block conflict/i); + + const insert = client.queries.find((query) => query.text.includes("INSERT INTO processed_blocks")); + expect(insert?.text).toContain("WHERE processed_blocks.chain_id = EXCLUDED.chain_id"); + expect(insert?.text).toContain("processed_blocks.block_hash = EXCLUDED.block_hash"); + }); +}); + +describe("bulk staging merge writer", () => { + it("stages decoded rows, merges canonical tables in dependency order, and advances the cursor after merge SQL", async () => { + const client = new FakeStageClient(); + + await stageAndMergeBatch(client as never, { + batchId: "00000000-0000-4000-8000-000000000001", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + writeCandlesInline: false, + enqueueSnapshots: true, + blocks: [{ + chainId: "juno-1", + height: 39381355, + blockHash: "block-39381355", + parentHash: "block-39381354", + blockTime: "2026-07-01T03:01:00Z", + txCount: 1, + events: [ + { kind: "pool_created", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 0, factoryAddress: "juno1factory", pairAddress: "juno1pair", assetInfos: ["ujuno", "uusdc"], raw: {} }, + { kind: "swap", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 1, pairAddress: "juno1pair", trader: "juno1trader", offerAsset: "ujuno", offerAmount: "1", askAsset: "uusdc", returnAmount: "2", raw: {} }, + { kind: "provide", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 2, pairAddress: "juno1pair", provider: "juno1provider", assets: [{ asset: "ujuno", amount: "1" }], shareAmount: "1", raw: {} }, + { kind: "incentive", chainId: "juno-1", height: 39381355, blockTime: "2026-07-01T03:01:00Z", txHash: "tx", msgIndex: 0, eventIndex: 3, incentivesAddress: "juno1incentives", action: "bond", userAddress: "juno1user", amount: "1", raw: {} }, + ], + }], + }); + + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_processed_blocks"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_pools"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_swaps"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_liquidity_events"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO stage_incentive_events"))).toBe(true); + + const processedMergeIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO processed_blocks")); + const poolMergeIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO pools") && query.text.includes("FROM stage_pools")); + const swapMergeIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO swaps") && query.text.includes("FROM stage_swaps")); + const cursorIndex = client.queries.findIndex((query) => query.text.includes("UPDATE indexer_cursors")); + expect(processedMergeIndex).toBeGreaterThanOrEqual(0); + expect(poolMergeIndex).toBeGreaterThan(processedMergeIndex); + expect(swapMergeIndex).toBeGreaterThan(poolMergeIndex); + expect(cursorIndex).toBeGreaterThan(swapMergeIndex); + expect(client.queries[cursorIndex]?.values).toEqual(["astroport-juno-v1", 39381355, "block-39381355"]); + expect(client.queries.some((query) => query.text.includes("INSERT INTO candle_jobs"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO snapshot_jobs"))).toBe(true); + }); + + it("does not advance the cursor when a staging merge detects a processed block conflict", async () => { + const client = new FakeStageClient(); + client.processedBlockRowCount = 0; + + await expect(stageAndMergeBatch(client as never, { + batchId: "00000000-0000-4000-8000-000000000002", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + blocks: [{ chainId: "juno-1", height: 12, blockHash: "new", parentHash: "old", blockTime: "2026-07-01T03:01:00Z", txCount: 0, events: [] }], + })).rejects.toThrow(/processed block conflict/); + + expect(client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + }); + + it("rejects non-contiguous or forked staged block ranges before advancing the cursor", async () => { + const client = new FakeStageClient(); + + await expect(stageAndMergeBatch(client as never, { + batchId: "00000000-0000-4000-8000-000000000003", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + blocks: [ + { chainId: "juno-1", height: 12, blockHash: "block-12", parentHash: "block-11", blockTime: "2026-07-01T03:01:00Z", txCount: 0, events: [] }, + { chainId: "juno-1", height: 13, blockHash: "block-13", parentHash: "different-parent", blockTime: "2026-07-01T03:01:06Z", txCount: 0, events: [] }, + ], + })).rejects.toThrow(/parent hash mismatch/); + expect(client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + + const previousClient = new FakeStageClient(); + previousClient.previousBlockHash = "canonical-11"; + await expect(stageAndMergeBatch(previousClient as never, { + batchId: "00000000-0000-4000-8000-000000000004", + chainId: "juno-1", + cursorId: "astroport-juno-v1", + blocks: [{ chainId: "juno-1", height: 12, blockHash: "block-12", parentHash: "fork-11", blockTime: "2026-07-01T03:01:00Z", txCount: 0, events: [] }], + })).rejects.toThrow(/parent hash mismatch/); + expect(previousClient.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + }); +}); + +describe("swap candle writes", () => { + it("uses asset_metadata decimals for indexer candle price and volume math", async () => { + const client = new FakeCandleClient(); + + await writeNormalizedEvent(client as never, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1pair", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }); + + const swapInsert = client.queries.find((query) => query.text.includes("INSERT INTO swaps")); + expect(swapInsert?.values?.slice(0, 4)).toEqual(["juno-1", "pool-1", "juno1pair", 39381355]); + const metadata = client.queries.find((query) => query.text.includes("FROM asset_metadata")); + expect(metadata?.values).toEqual(["juno-1", ["factory/token18", "ujuno"]]); + const candleInsert = client.queries.find((query) => query.text.includes("INSERT INTO token_candles")); + expect(candleInsert?.values?.slice(3, 10)).toEqual(["factory/token18", "ujuno", "5m", "2026-07-01T03:00:00.000Z", "1.5", "2", "3"]); + }); + + it("skips candle writes when inline candle option is disabled", async () => { + const client = new FakeCandleClient(); + + await writeNormalizedEvent(client as never, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1pair", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }, { writeCandlesInline: false }); + + expect(client.queries.some((query) => query.text.includes("INSERT INTO swaps"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO candle_jobs"))).toBe(true); + const jobInsert = client.queries.find((query) => query.text.includes("INSERT INTO candle_jobs")); + expect(jobInsert?.values).toEqual(["juno-1", "juno1pair", "2026-07-01T00:00:00.000Z", "2026-07-02T00:00:00.000Z"]); + expect(client.queries.some((query) => query.text.includes("FROM asset_metadata"))).toBe(false); + expect(client.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + + it("skips candle writes when either swap asset lacks valid decimals", async () => { + const client = new FakeCandleClient([{ asset: "factory/missing18", decimals: 18 }, { asset: "ujuno-missing", decimals: null }]); + + await writeNormalizedEvent(client as never, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1pair", + offerAsset: "factory/missing18", + offerAmount: "2000000000000000000", + askAsset: "ujuno-missing", + returnAmount: "3000000", + raw: {}, + }); + + expect(client.queries.some((query) => query.text.includes("INSERT INTO swaps"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + + it("uses decimals for candle backfills and skips degraded metadata", async () => { + const okClient = new FakeCandleClient([{ asset: "factory/backfill18", decimals: 18 }, { asset: "ujuno-backfill", decimals: 6 }]); + await expect(backfillTokenCandles(okClient as never, { chainId: "juno-1" })).resolves.toBe(1); + const backfillInsert = okClient.queries.find((query) => query.text.includes("INSERT INTO token_candles")); + expect(backfillInsert?.values?.slice(3, 15)).toEqual(["factory/backfill18", "ujuno-backfill", "5m", "2026-07-01T03:00:00.000Z", "1.5", "1.5", "1.5", "1.5", "2", "3", 1, "backfill"]); + + const badClient = new FakeCandleClient( + [{ asset: "factory/backfill-bad18", decimals: 309 }, { asset: "ujuno-backfill-bad", decimals: 6 }], + { pair_address: "juno1pair", block_time: "2026-07-01T03:01:00Z", offer_asset: "factory/backfill-bad18", offer_amount: "2000000000000000000", ask_asset: "ujuno-backfill-bad", return_amount: "3000000", height: "39381355", tx_hash: "tx", msg_index: "0", event_index: "0" }, + ); + await expect(backfillTokenCandles(badClient as never, { chainId: "juno-1" })).resolves.toBe(1); + expect(badClient.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + + it("worker claims a candle job, rebuilds candles through shared helper, and marks completion", async () => { + const client = new FakeCandleClient([{ asset: "factory/backfill18", decimals: 18 }, { asset: "ujuno-backfill", decimals: 6 }]); + + await expect(processNextCandleJob(client as never, { chainId: "juno-1", workerId: "worker-1" })).resolves.toMatchObject({ + id: "job-1", + pairAddress: "juno1pair", + }); + + const claim = client.queries.find((query) => query.text.includes("FOR UPDATE SKIP LOCKED")); + expect(claim?.values?.slice(0, 2)).toEqual(["juno-1", "worker-1"]); + const swapRead = client.queries.find((query) => query.text.includes("FROM swaps")); + expect(swapRead?.text).toContain("ORDER BY height ASC, msg_index ASC, event_index ASC, id ASC"); + expect(swapRead?.values).toEqual(["juno-1", "juno1pair", "2026-07-01T00:00:00.000Z", "2026-07-02T00:00:00.000Z", 2147483647, true]); + const candleInsert = client.queries.find((query) => query.text.includes("INSERT INTO token_candles")); + expect(candleInsert?.values?.slice(3, 15)).toEqual(["factory/backfill18", "ujuno-backfill", "5m", "2026-07-01T03:00:00.000Z", "1.5", "1.5", "1.5", "1.5", "2", "3", 1, "worker"]); + const complete = client.queries.find((query) => query.text.includes("rerun_requested")); + expect(complete?.text).toContain("AND status = 'running'"); + expect(complete?.text).toContain("AND worker_id = $2"); + expect(complete?.text).toContain("AND attempts = $3"); + expect(complete?.values).toEqual(["job-1", "worker-1", 1, 1]); + }); + it("writes pool discovery before same-batch pair events regardless of emitted order", async () => { + const client = new FakeCandleClient(undefined, undefined, []); + + await writeNormalizedEvents(client as never, "juno-1", [ + { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1newpair", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }, + { + kind: "pool_created", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 1, + factoryAddress: "juno1factory", + pairAddress: "juno1newpair", + assetInfos: ["factory/token18", "ujuno"], + raw: {}, + }, + ]); + + const poolInsertIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO pools")); + const swapInsertIndex = client.queries.findIndex((query) => query.text.includes("INSERT INTO swaps")); + expect(poolInsertIndex).toBeGreaterThanOrEqual(0); + expect(swapInsertIndex).toBeGreaterThan(poolInsertIndex); + const swapInsert = client.queries[swapInsertIndex]; + expect(swapInsert?.values?.slice(0, 4)).toEqual(["juno-1", "pool-created", "juno1newpair", 39381355]); + }); + + it("skips swap persistence for unknown pair contracts", async () => { + const client = new FakeCandleClient(undefined, undefined, []); + + await writeNormalizedEvent(client as never, "juno-1", { + kind: "swap", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx", + msgIndex: 0, + eventIndex: 0, + pairAddress: "juno1unrelated", + offerAsset: "factory/token18", + offerAmount: "2000000000000000000", + askAsset: "ujuno", + returnAmount: "3000000", + raw: {}, + }); + + expect(client.queries.some((query) => query.text.includes("FROM pools"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO swaps"))).toBe(false); + expect(client.queries.some((query) => query.text.includes("INSERT INTO token_candles"))).toBe(false); + }); + + it("writes known liquidity events with pool_id", async () => { + const client = new FakeCandleClient(); + + await writeNormalizedEvent(client as never, "juno-1", { + kind: "provide", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx-liq-known", + msgIndex: 0, + eventIndex: 1, + pairAddress: "juno1pair", + provider: "juno1provider", + assets: [{ asset: "ujuno", amount: "1" }], + shareAmount: "1", + raw: {}, + }); + + const insert = client.queries.find((query) => query.text.includes("INSERT INTO liquidity_events")); + expect(insert?.values?.slice(0, 4)).toEqual(["juno-1", "pool-1", "juno1pair", 39381355]); + }); + + it("skips liquidity persistence for unknown pair contracts", async () => { + const client = new FakeCandleClient(undefined, undefined, []); + + await writeNormalizedEvent(client as never, "juno-1", { + kind: "provide", + chainId: "juno-1", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + txHash: "tx-liq", + msgIndex: 0, + eventIndex: 1, + pairAddress: "juno1unrelated", + provider: "juno1provider", + assets: [{ asset: "ujuno", amount: "1" }], + shareAmount: "1", + raw: {}, + }); + + expect(client.queries.some((query) => query.text.includes("FROM pools"))).toBe(true); + expect(client.queries.some((query) => query.text.includes("INSERT INTO liquidity_events"))).toBe(false); + }); +}); + +describe("pool state snapshots", () => { + it("enqueues reserve snapshot jobs idempotently for known pools only", async () => { + const client = new FakeBlockClient(); + + await expect(enqueueSnapshotJobs(client as never, { + chainId: "juno-1", + pairAddresses: ["juno1pair", "juno1pair", "juno1missing"], + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + reason: "touched", + })).resolves.toBe(1); + + const insert = client.queries.find((query) => query.text.includes("INSERT INTO snapshot_jobs")); + expect(insert?.text).toContain("FROM pools p"); + expect(insert?.text).toContain("ON CONFLICT (chain_id, pair_address, height, reason) DO NOTHING"); + expect(insert?.values).toEqual(["juno-1", ["juno1pair", "juno1missing"], 39381355, "2026-07-01T03:01:00Z", "touched"]); + }); + + it("claims snapshot jobs with skip-locked leases and updates terminal state", async () => { + const client = new FakeBlockClient(); + + await expect(claimSnapshotJobs(client as never, { chainId: "juno-1", limit: 10, leaseSeconds: 30, maxAttempts: 5 })).resolves.toEqual([ + { id: "7", chainId: "juno-1", pairAddress: "juno1pair", height: 39381355, blockTime: "2026-07-01T03:01:00Z", reason: "touched", status: "leased", attempts: 1 }, + ]); + await markSnapshotJobSucceeded(client as never, { jobId: "7", attempt: 1 }); + await markSnapshotJobFailed(client as never, { jobId: "8", attempt: 2, error: "temporary", permanent: false, maxAttempts: 5 }); + + const claim = client.queries.find((query) => query.text.includes("WITH claimable")); + expect(claim?.text).toContain("FOR UPDATE SKIP LOCKED"); + expect(claim?.values).toEqual(["juno-1", 10, "30 seconds", 5]); + const success = client.queries.find((query) => query.text.includes("status = 'succeeded'")); + expect(success?.text).toContain("AND status = 'leased'"); + expect(success?.text).toContain("AND attempts = $2"); + expect(success?.values).toEqual(["7", 1]); + const failure = client.queries.find((query) => query.text.includes("last_error = $4")); + expect(failure?.text).toContain("AND status = 'leased'"); + expect(failure?.text).toContain("AND attempts = $2"); + expect(failure?.values).toEqual(["8", 2, false, "temporary", 5]); + }); + + it("upserts reserve snapshots idempotently by pool, height, and source", async () => { + const client = new FakeBlockClient(); + client.rowsByKey.set("pool:juno1pair", [{ id: "pool-1" }]); + + await upsertPoolStateSnapshot(client as never, { + chainId: "juno-1", + pairAddress: "juno1pair", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + reserves: [{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }], + totalShare: "789", + source: "event", + }); + + const select = client.queries.find((query) => query.text.includes("FROM pools")); + expect(select?.values).toEqual(["juno-1", "juno1pair"]); + const insert = client.queries.find((query) => query.text.includes("INSERT INTO pool_state_snapshots")); + expect(insert?.text).toContain("ON CONFLICT (pool_id, height, source) DO UPDATE"); + expect(insert?.values).toEqual(["pool-1", 39381355, "2026-07-01T03:01:00Z", JSON.stringify([{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }]), "789", "event"]); + }); + + it("rejects snapshots for unknown pools instead of writing orphan state", async () => { + const client = new FakeBlockClient(); + + await expect(upsertPoolStateSnapshot(client as never, { + chainId: "juno-1", + pairAddress: "juno1missing", + height: 39381355, + blockTime: "2026-07-01T03:01:00Z", + reserves: [], + })).rejects.toThrow(/unknown pair juno1missing/i); + }); +}); + +describe("API read model refresh", () => { + it("calls the SQL refresh helper and maps affected rows", async () => { + const client = { + queries: [] as Query[], + async query>(text: string, values?: unknown[]): Promise<{ rows: T[]; rowCount: number }> { + this.queries.push({ text, values }); + return { rows: [{ model: "latest_pool_state", rows_affected: "1" }, { model: "protocol_stats_latest", rows_affected: 1 }] as T[], rowCount: 2 }; + }, + }; + + await expect(refreshApiReadModels(client as never, { chainId: "juno-1" })).resolves.toEqual([ + { model: "latest_pool_state", rowsAffected: 1 }, + { model: "protocol_stats_latest", rowsAffected: 1 }, + ]); + expect(client.queries[0]).toEqual({ text: "SELECT model, rows_affected FROM refresh_api_read_models($1::text)", values: ["juno-1"] }); + }); +}); diff --git a/indexer/test/events.test.ts b/indexer/test/events.test.ts new file mode 100644 index 000000000..01cd3f106 --- /dev/null +++ b/indexer/test/events.test.ts @@ -0,0 +1,177 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { attributesToRecord, normalizeBlockEvents, normalizeWasmEvent } from "../src/events.js"; + +const context = { chainId: "juno-1", height: 123, blockTime: "2026-07-02T00:00:00Z", txHash: "ABC", msgIndex: 0, eventIndex: 0 }; +const contracts = { factoryAddress: "juno1factory", incentivesAddress: "juno1incentives" }; +const factoryAddress = "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca"; +const incentivesAddress = "juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598"; +const pairAddress = "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv"; +const testDenom = "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323"; +const junoV1Contracts = { factoryAddress, incentivesAddress }; + +type Fixture = { height: number; txhash: string; timestamp: string; events: Array<{ type: string; attributes: Array<{ key: string; value: string; index?: boolean }> }> }; + +function fixture(name: string): Fixture { + return JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", "juno-v1", `${name}.json`), "utf8")) as Fixture; +} + +function normalizedFixture(name: string) { + const tx = fixture(name); + return normalizeBlockEvents(tx.events, { chainId: "juno-1", height: tx.height, blockTime: tx.timestamp, txHash: tx.txhash }, junoV1Contracts); +} + +describe("event normalization", () => { + it("preserves repeated attributes", () => { + expect(attributesToRecord([ + { key: "asset_info", value: "ujuno" }, + { key: "asset_info", value: "factory/token" }, + ])).toEqual({ asset_info: ["ujuno", "factory/token"] }); + }); + + it("normalizes factory pair creation events", () => { + const event = normalizeWasmEvent({ + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1factory" }, + { key: "action", value: "create_pair" }, + { key: "pair_contract_addr", value: "juno1pair" }, + { key: "liquidity_token_addr", value: "factory/juno1pair/astroport/share" }, + { key: "pair_type", value: "xyk" }, + { key: "asset_info", value: "ujuno" }, + { key: "asset_info", value: "factory/juno/token" }, + ], + }, context, contracts); + + expect(event).toMatchObject({ + kind: "pool_created", + pairAddress: "juno1pair", + liquidityTokenAddress: "factory/juno1pair/astroport/share", + poolType: "xyk", + assetInfos: ["ujuno", "factory/juno/token"], + }); + }); + + it("normalizes swap events emitted by pair contracts", () => { + const event = normalizeWasmEvent({ + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000" }, + { key: "ask_asset", value: "factory/juno/token" }, + { key: "return_amount", value: "990" }, + { key: "commission_amount", value: "3" }, + ], + }, context, contracts); + + expect(event).toMatchObject({ + kind: "swap", + pairAddress: "juno1pair", + trader: "juno1trader", + offerAsset: "ujuno", + offerAmount: "1000", + returnAmount: "990", + commissionAmount: "3", + }); + }); + + it("normalizes provide and withdraw liquidity events", () => { + const events = normalizeBlockEvents([ + { + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "provide_liquidity" }, + { key: "sender", value: "juno1lp" }, + { key: "asset", value: "ujuno" }, + { key: "asset", value: "factory/juno/token" }, + { key: "amount", value: "1000" }, + { key: "amount", value: "2000" }, + { key: "share", value: "1414" }, + ], + }, + { + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "withdraw_liquidity" }, + { key: "sender", value: "juno1lp" }, + { key: "share", value: "100" }, + ], + }, + ], { chainId: "juno-1", height: 124, blockTime: "2026-07-02T00:01:00Z", txHash: "DEF" }, contracts); + + expect(events.map((event) => event.kind)).toEqual(["provide", "withdraw"]); + expect(events[0]).toMatchObject({ provider: "juno1lp", assets: [{ asset: "ujuno", amount: "1000" }, { asset: "factory/juno/token", amount: "2000" }] }); + }); + + it("normalizes incentives events from the configured incentives contract", () => { + const event = normalizeWasmEvent({ + type: "wasm", + attributes: [ + { key: "_contract_address", value: "juno1incentives" }, + { key: "action", value: "deposit" }, + { key: "sender", value: "juno1staker" }, + { key: "lp_token", value: "factory/juno1pair/astroport/share" }, + { key: "amount", value: "500" }, + ], + }, context, contracts); + + expect(event).toMatchObject({ + kind: "incentive", + action: "deposit", + userAddress: "juno1staker", + lpTokenAddress: "factory/juno1pair/astroport/share", + amount: "500", + }); + }); + + it("normalizes the real Juno v1 create-pair deployment tx fixture", () => { + const events = normalizedFixture("create-pair"); + const created = events.find((event) => event.kind === "pool_created"); + expect(created).toMatchObject({ + kind: "pool_created", + height: 39381305, + txHash: "8EFD15276286C15D5CFF11B55D49522D2987E16F8220DE671CA0971E586BCD8E", + factoryAddress, + pairAddress, + }); + }); + + it("normalizes real Juno v1 liquidity tx fixtures with asset amounts", () => { + const seed = normalizedFixture("seed-liquidity"); + expect(seed).toHaveLength(1); + expect(seed[0]).toMatchObject({ + kind: "provide", + pairAddress, + shareAmount: "9999000", + assets: [{ amount: "10000000", asset: "ujuno" }, { amount: "10000000", asset: testDenom }], + }); + + const add = normalizedFixture("smoke-add-liquidity"); + expect(add[0]).toMatchObject({ kind: "provide", shareAmount: "9900", assets: [{ amount: "10000", asset: "ujuno" }, { amount: "9803", asset: testDenom }] }); + + const withdraw = normalizedFixture("smoke-withdraw-liquidity"); + expect(withdraw[0]).toMatchObject({ kind: "withdraw", shareAmount: "1000", assets: [{ amount: "1010", asset: "ujuno" }, { amount: "990", asset: testDenom }] }); + }); + + it("normalizes the real Juno v1 smoke swap tx fixture", () => { + const events = normalizedFixture("smoke-swap"); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + kind: "swap", + pairAddress, + trader: "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + offerAsset: "ujuno", + askAsset: testDenom, + offerAmount: "100000", + returnAmount: "98712", + spreadAmount: "991", + commissionAmount: "297", + }); + }); +}); diff --git a/indexer/test/fixtures/juno-v1/create-pair.json b/indexer/test/fixtures/juno-v1/create-pair.json new file mode 100644 index 000000000..9521fd861 --- /dev/null +++ b/indexer/test/fixtures/juno-v1/create-pair.json @@ -0,0 +1,112 @@ +{ + "height": 39381305, + "txhash": "8EFD15276286C15D5CFF11B55D49522D2987E16F8220DE671CA0971E586BCD8E", + "timestamp": "2026-07-01T03:27:14Z", + "events": [ + { + "type": "execute", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca", + "index": true + }, + { + "key": "action", + "value": "create_pair", + "index": true + }, + { + "key": "pair", + "value": "ujuno-factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "asset_balances_tracking", + "value": "disabled", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "lp_denom", + "value": "factory/juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv/astroport/share", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca", + "index": true + }, + { + "key": "action", + "value": "register", + "index": true + }, + { + "key": "pair_contract_addr", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + } + ] +} diff --git a/indexer/test/fixtures/juno-v1/seed-liquidity.json b/indexer/test/fixtures/juno-v1/seed-liquidity.json new file mode 100644 index 000000000..f5f1f388d --- /dev/null +++ b/indexer/test/fixtures/juno-v1/seed-liquidity.json @@ -0,0 +1,62 @@ +{ + "height": 39381310, + "txhash": "DEE44565B5E6124A27430646A371691396A504497EB0315D4A66675C8C765401", + "timestamp": "2026-07-01T03:27:27Z", + "events": [ + { + "type": "execute", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "action", + "value": "provide_liquidity", + "index": true + }, + { + "key": "sender", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "receiver", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "assets", + "value": "10000000ujuno, 10000000factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + "index": true + }, + { + "key": "share", + "value": "9999000", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + } + ] +} diff --git a/indexer/test/fixtures/juno-v1/smoke-add-liquidity.json b/indexer/test/fixtures/juno-v1/smoke-add-liquidity.json new file mode 100644 index 000000000..3ef942f3b --- /dev/null +++ b/indexer/test/fixtures/juno-v1/smoke-add-liquidity.json @@ -0,0 +1,62 @@ +{ + "height": 39381353, + "txhash": "F6C33B16578AAA1A4DC4931C250A07649F0F750AC8EAAE150146F5C6D54C5079", + "timestamp": "2026-07-01T03:29:22Z", + "events": [ + { + "type": "execute", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "action", + "value": "provide_liquidity", + "index": true + }, + { + "key": "sender", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "receiver", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "assets", + "value": "10000ujuno, 9803factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + "index": true + }, + { + "key": "share", + "value": "9900", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + } + ] +} diff --git a/indexer/test/fixtures/juno-v1/smoke-swap.json b/indexer/test/fixtures/juno-v1/smoke-swap.json new file mode 100644 index 000000000..6dac45945 --- /dev/null +++ b/indexer/test/fixtures/juno-v1/smoke-swap.json @@ -0,0 +1,92 @@ +{ + "height": 39381352, + "txhash": "15CE5277D55668B4ADE7D44132C7E2EE4FE71882B2003C28503AF09D7138B502", + "timestamp": "2026-07-01T03:29:19Z", + "events": [ + { + "type": "execute", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "action", + "value": "swap", + "index": true + }, + { + "key": "sender", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "receiver", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "offer_asset", + "value": "ujuno", + "index": true + }, + { + "key": "ask_asset", + "value": "factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + "index": true + }, + { + "key": "offer_amount", + "value": "100000", + "index": true + }, + { + "key": "return_amount", + "value": "98712", + "index": true + }, + { + "key": "spread_amount", + "value": "991", + "index": true + }, + { + "key": "commission_amount", + "value": "297", + "index": true + }, + { + "key": "maker_fee_amount", + "value": "0", + "index": true + }, + { + "key": "fee_share_amount", + "value": "0", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + } + ] +} diff --git a/indexer/test/fixtures/juno-v1/smoke-withdraw-liquidity.json b/indexer/test/fixtures/juno-v1/smoke-withdraw-liquidity.json new file mode 100644 index 000000000..52da87474 --- /dev/null +++ b/indexer/test/fixtures/juno-v1/smoke-withdraw-liquidity.json @@ -0,0 +1,57 @@ +{ + "height": 39381355, + "txhash": "ED1923D1DF041245296358BEC1EF80FDEFF47A3118F8CBD06EB73A7F5E860E97", + "timestamp": "2026-07-01T03:29:31Z", + "events": [ + { + "type": "execute", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "wasm", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv", + "index": true + }, + { + "key": "action", + "value": "withdraw_liquidity", + "index": true + }, + { + "key": "sender", + "value": "juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76", + "index": true + }, + { + "key": "withdrawn_share", + "value": "1000", + "index": true + }, + { + "key": "refund_assets", + "value": "1010ujuno, 990factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + } + ] +} diff --git a/indexer/test/indexer.test.ts b/indexer/test/indexer.test.ts new file mode 100644 index 000000000..950533818 --- /dev/null +++ b/indexer/test/indexer.test.ts @@ -0,0 +1,318 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_CONTRACTS, type IndexerConfig } from "../src/config.js"; +import { Indexer } from "../src/indexer.js"; + +type Query = { text: string; values?: unknown[] }; + +class FakeIndexerClient { + queries: Query[] = []; + failProcessedBlockHeight?: number; + failStagedMerge = false; + onBegin?: () => void; + + async query>(text: string, values?: unknown[]): Promise<{ rows: T[]; rowCount: number }> { + this.queries.push({ text, values }); + if (text === "BEGIN") this.onBegin?.(); + if (text.includes("RETURNING last_height")) return { rows: [{ last_height: "10" }] as T[], rowCount: 1 }; + if (text.includes("FROM processed_blocks")) return { rows: [] as T[], rowCount: 0 }; + if (text.includes("INSERT INTO processed_blocks") && text.includes("FROM stage_processed_blocks")) { + if (this.failStagedMerge) throw new Error("staged merge failed"); + return { rows: [] as T[], rowCount: 1 }; + } + if (text.includes("INSERT INTO processed_blocks")) { + if (values?.[1] === this.failProcessedBlockHeight) throw new Error(`boom at ${this.failProcessedBlockHeight}`); + return { rows: [] as T[], rowCount: 1 }; + } + if (text.includes("INSERT INTO swaps")) return { rows: [{ id: "swap-1", pool_id: "pool-1" }] as T[], rowCount: 1 }; + if (text.includes("FROM asset_metadata")) return { rows: [{ asset: "ujuno", decimals: 6 }, { asset: "uusdc", decimals: 6 }] as T[], rowCount: 2 }; + if (text.includes("FROM pools") && text.includes("ANY($2::text[])")) { + const requested = new Set((values?.[1] as string[]) ?? []); + const rows = ["juno1pair"].filter((pair) => requested.has(pair)).map((pair_address) => ({ pair_address })); + return { rows: rows as T[], rowCount: rows.length }; + } + if (text.includes("FROM pools") && text.includes("pair_address")) return { rows: [{ id: "pool-1", pair_address: "juno1pair" }] as T[], rowCount: 1 }; + if (text.includes("INSERT INTO token_candles")) return { rows: [] as T[], rowCount: 1 }; + if (text.includes("INSERT INTO pool_state_snapshots")) return { rows: [] as T[], rowCount: 1 }; + if (text.includes("INSERT INTO snapshot_jobs")) return { rows: [] as T[], rowCount: 1 }; + if (text.includes("UPDATE indexer_cursors")) return { rows: [] as T[], rowCount: 1 }; + return { rows: [] as T[], rowCount: 0 }; + } + + release() {} +} + +class FakeIndexerPool { + readonly client = new FakeIndexerClient(); + async connect() { return this.client; } +} + +const baseConfig: IndexerConfig = { + databaseUrl: "postgres://test", + rpcUrl: "https://rpc.example", + restUrl: "https://lcd.example", + wsUrl: "wss://rpc.example/websocket", + chainId: "juno-1", + factoryAddress: DEFAULT_CONTRACTS.factory, + routerAddress: DEFAULT_CONTRACTS.router, + incentivesAddress: DEFAULT_CONTRACTS.incentives, + oracleAddress: DEFAULT_CONTRACTS.oracle, + nativeCoinRegistryAddress: DEFAULT_CONTRACTS.nativeCoinRegistry, + startHeight: 11, + confirmationDepth: 2, + pollIntervalMs: 1, + batchSize: 1, + dryRun: false, + cursorId: "astroport-juno-v1", + indexerMode: "realtime", + rangeSize: 5_000, + fetchWindowSize: 250, + fetchConcurrency: 32, + realtimeFetchConcurrency: 8, + rpcTimeoutMs: 10_000, + rpcMaxRetries: 5, + ingestCandlesInline: true, + ingestReserveSnapshotsInline: true, + ingestAggregatesInline: false, + ingestBulkStagingEnabled: false, + priceProviderName: "provider", + priceCacheTtlMs: 300_000, + priceStaleAfterMs: 1_800_000, + priceAllowStale: true, + priceDevMocks: false, + readModelRefreshIntervalMs: 0, + apiPort: 8787, +}; + +afterEach(() => vi.restoreAllMocks()); + +function rpcBlock(height: number, txEvents: unknown[] = []) { + return { + block: { result: { block_id: { hash: `block-${height}` }, block: { header: { time: `2026-07-01T03:00:${String(height).padStart(2, "0")}Z`, last_block_id: { hash: `block-${height - 1}` } }, data: { txs: txEvents.length > 0 ? ["AA=="] : [] } } } }, + results: { result: { txs_results: txEvents.length > 0 ? [{ hash: `tx-${height}`, events: txEvents }] : [] } }, + }; +} + +function mockRpcRange(headHeight: number, blocks: Map, fetchedBlocks?: number[], onBlockFetchStart?: () => void, onBlockFetchEnd?: () => void) { + return vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => { + const url = String(input); + if (url === "https://rpc.example/status") { + return { ok: true, json: async () => ({ result: { sync_info: { latest_block_height: String(headHeight), latest_block_hash: "head" } } }) } as Response; + } + const blockMatch = url.match(/^https:\/\/rpc\.example\/block\?height=(\d+)$/); + if (blockMatch) { + const height = Number(blockMatch[1]); + fetchedBlocks?.push(height); + onBlockFetchStart?.(); + await Promise.resolve(); + onBlockFetchEnd?.(); + return { ok: true, json: async () => blocks.get(height)?.block } as Response; + } + const resultsMatch = url.match(/^https:\/\/rpc\.example\/block_results\?height=(\d+)$/); + if (resultsMatch) { + const height = Number(resultsMatch[1]); + return { ok: true, json: async () => blocks.get(height)?.results } as Response; + } + throw new Error(`unexpected fetch: ${url}`); + }); +} + +describe("Indexer fetch/decode/ordered writer pipeline", () => { + it("fetches multiple blocks before ordered writing and advances the cursor height by height", async () => { + const blocks = new Map([11, 12, 13].map((height) => [height, rpcBlock(height)])); + const fetchedBlocks: number[] = []; + mockRpcRange(15, blocks, fetchedBlocks); + const pool = new FakeIndexerPool(); + const fetchedBeforeFirstWrite: number[] = []; + pool.client.onBegin = () => { + if (fetchedBeforeFirstWrite.length === 0) fetchedBeforeFirstWrite.push(...fetchedBlocks); + }; + + await expect(new Indexer({ ...baseConfig, batchSize: 3, fetchConcurrency: 1, realtimeFetchConcurrency: 3 }, pool as never).runOnce()).resolves.toMatchObject({ processed: 3, cursorHeight: 13 }); + + expect(fetchedBeforeFirstWrite.sort((a, b) => a - b)).toEqual([11, 12, 13]); + const cursorUpdates = pool.client.queries.filter((query) => query.text.includes("UPDATE indexer_cursors")); + expect(cursorUpdates.map((query) => query.values?.[1])).toEqual([11, 12, 13]); + }); + + it("uses catchup fetch concurrency when the indexer is in catchup mode", async () => { + const blocks = new Map([11, 12, 13, 14].map((height) => [height, rpcBlock(height)])); + let activeBlockFetches = 0; + let maxActiveBlockFetches = 0; + mockRpcRange( + 16, + blocks, + undefined, + () => { + activeBlockFetches += 1; + maxActiveBlockFetches = Math.max(maxActiveBlockFetches, activeBlockFetches); + }, + () => { + activeBlockFetches -= 1; + }, + ); + const pool = new FakeIndexerPool(); + + await expect(new Indexer({ ...baseConfig, indexerMode: "catchup", batchSize: 4, fetchConcurrency: 2, realtimeFetchConcurrency: 4 }, pool as never).runOnce()).resolves.toMatchObject({ processed: 4, cursorHeight: 14 }); + + expect(maxActiveBlockFetches).toBe(2); + }); + + it("stops later cursor advancement when an ordered block write fails", async () => { + const blocks = new Map([11, 12, 13].map((height) => [height, rpcBlock(height)])); + mockRpcRange(15, blocks); + const pool = new FakeIndexerPool(); + pool.client.failProcessedBlockHeight = 12; + + await expect(new Indexer({ ...baseConfig, batchSize: 3, realtimeFetchConcurrency: 3 }, pool as never).runOnce()).rejects.toThrow(/boom at 12/); + + const cursorUpdates = pool.client.queries.filter((query) => query.text.includes("UPDATE indexer_cursors")); + expect(cursorUpdates.map((query) => query.values?.[1])).toEqual([11]); + }); + + it("uses the bulk staging writer only for catchup mode when enabled", async () => { + const blocks = new Map([[11, rpcBlock(11, [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }])]]); + mockRpcRange(13, blocks); + const pool = new FakeIndexerPool(); + + await expect(new Indexer({ ...baseConfig, indexerMode: "catchup", ingestBulkStagingEnabled: true, ingestCandlesInline: false, batchSize: 1, ingestReserveSnapshotsInline: false }, pool as never).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO stage_processed_blocks"))).toBe(true); + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO swaps") && query.text.includes("FROM stage_swaps"))).toBe(true); + const cursorUpdates = pool.client.queries.filter((query) => query.text.includes("UPDATE indexer_cursors")); + expect(cursorUpdates.map((query) => query.values?.[1])).toEqual([11]); + }); + + it("leaves the cursor unchanged when the bulk staging merge fails", async () => { + const blocks = new Map([[11, rpcBlock(11)]]); + mockRpcRange(13, blocks); + const pool = new FakeIndexerPool(); + pool.client.failStagedMerge = true; + + await expect(new Indexer({ ...baseConfig, indexerMode: "catchup", ingestBulkStagingEnabled: true, ingestCandlesInline: false, batchSize: 1 }, pool as never).runOnce()).rejects.toThrow(/staged merge failed/); + + expect(pool.client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(false); + expect(pool.client.queries.some((query) => query.text === "ROLLBACK")).toBe(true); + }); +}); + +describe("Indexer reserve snapshots", () => { + it("queries pair pool state at the processed height and writes one lcd snapshot per touched pair", async () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const url = String(input); + if (url === "https://rpc.example/status") { + return { ok: true, json: async () => ({ result: { sync_info: { latest_block_height: "13", latest_block_hash: "head" } } }) } as Response; + } + if (url === "https://rpc.example/block?height=11") { + return { ok: true, json: async () => ({ result: { block_id: { hash: "block-11" }, block: { header: { time: "2026-07-01T03:01:00Z", last_block_id: { hash: "block-10" } }, data: { txs: ["AA=="] } } } }) } as Response; + } + if (url === "https://rpc.example/block_results?height=11") { + return { ok: true, json: async () => ({ result: { txs_results: [{ hash: "tx", events: [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }] }] } }) } as Response; + } + if (url.startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/")) { + expect((init as RequestInit | undefined)?.headers).toMatchObject({ "x-cosmos-block-height": "11" }); + return { ok: true, json: async () => ({ data: { assets: [{ info: { native_token: { denom: "ujuno" } }, amount: "123" }, { info: { native_token: { denom: "uusdc" } }, amount: "456" }], total_share: "789" } }) } as Response; + } + throw new Error(`unexpected fetch: ${url}`); + }); + const pool = new FakeIndexerPool(); + + await expect(new Indexer(baseConfig, pool as never).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + + const snapshot = pool.client.queries.find((query) => query.text.includes("INSERT INTO pool_state_snapshots")); + expect(snapshot?.values).toEqual(["pool-1", 11, "2026-07-01T03:01:00Z", JSON.stringify([{ denom: "ujuno", amount: "123" }, { denom: "uusdc", amount: "456" }]), "789", "lcd"]); + const lcdCalls = fetchSpy.mock.calls.filter(([input]) => String(input).startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/")); + expect(lcdCalls).toHaveLength(1); + const rangeLog = JSON.parse(String(logSpy.mock.calls.at(-1)?.[0])); + expect(rangeLog).toMatchObject({ + msg: "indexer_range_processed", + role: "indexer", + rangeFrom: 11, + rangeTo: 11, + cursor: 11, + head: 13, + target: 11, + lag: 0, + blocks: 1, + swaps: 1, + liquidityEvents: 0, + incentiveEvents: 0, + }); + expect(rangeLog.durationMs).toEqual(expect.any(Number)); + expect(rangeLog.dbDurationMs).toEqual(expect.any(Number)); + }); + + it("keeps cursor progress when LCD reserve snapshots fail", async () => { + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(console, "warn").mockImplementation(() => undefined); + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/status") { + return { ok: true, json: async () => ({ result: { sync_info: { latest_block_height: "13", latest_block_hash: "head" } } }) } as Response; + } + if (url === "https://rpc.example/block?height=11") { + return { ok: true, json: async () => ({ result: { block_id: { hash: "block-11" }, block: { header: { time: "2026-07-01T03:01:00Z", last_block_id: { hash: "block-10" } }, data: { txs: ["AA=="] } } } }) } as Response; + } + if (url === "https://rpc.example/block_results?height=11") { + return { ok: true, json: async () => ({ result: { txs_results: [{ hash: "tx", events: [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }] }] } }) } as Response; + } + if (url.startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/")) { + return { ok: false, status: 500, statusText: "unavailable", json: async () => ({}) } as Response; + } + throw new Error(`unexpected fetch: ${url}`); + }); + const pool = new FakeIndexerPool(); + + await expect(new Indexer(baseConfig, pool as never).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + + expect(pool.client.queries.some((query) => query.text.includes("UPDATE indexer_cursors"))).toBe(true); + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO pool_state_snapshots"))).toBe(false); + expect(fetchSpy.mock.calls.filter(([input]) => String(input).startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/"))).toHaveLength(3); + }); + + it("does not call LCD pool state when inline reserve snapshots are disabled", async () => { + const swapEvents = [{ type: "wasm", attributes: [ + { key: "_contract_address", value: "juno1pair" }, + { key: "action", value: "swap" }, + { key: "sender", value: "juno1trader" }, + { key: "offer_asset", value: "ujuno" }, + { key: "offer_amount", value: "1000000" }, + { key: "ask_asset", value: "uusdc" }, + { key: "return_amount", value: "2000000" }, + ] }]; + const blocks = new Map([[11, rpcBlock(11, swapEvents)]]); + const fetchSpy = mockRpcRange(13, blocks); + const pool = new FakeIndexerPool(); + + await expect(new Indexer({ ...baseConfig, ingestReserveSnapshotsInline: false }, pool as never).runOnce()).resolves.toMatchObject({ processed: 1, cursorHeight: 11 }); + + expect(pool.client.queries.some((query) => query.text.includes("INSERT INTO pool_state_snapshots"))).toBe(false); + const jobInsert = pool.client.queries.find((query) => query.text.includes("INSERT INTO snapshot_jobs")); + expect(jobInsert?.values).toEqual(["juno-1", ["juno1pair"], 11, "2026-07-01T03:00:11Z", "touched"]); + expect(fetchSpy.mock.calls.some(([input]) => String(input).startsWith("https://lcd.example/cosmwasm/wasm/v1/contract/juno1pair/smart/"))).toBe(false); + }); +}); diff --git a/indexer/test/ranges.test.ts b/indexer/test/ranges.test.ts new file mode 100644 index 000000000..c4a617985 --- /dev/null +++ b/indexer/test/ranges.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { Indexer } from "../src/indexer.js"; +import { parseNonNegativeInteger, nextBlockRange } from "../src/ranges.js"; +import type { IndexerConfig } from "../src/config.js"; + +const testConfig: IndexerConfig = { + databaseUrl: "postgres://postgres:***@localhost:5432/astroport_indexer_test", + rpcUrl: "http://127.0.0.1:26657", + restUrl: "http://127.0.0.1:1317", + wsUrl: "ws://127.0.0.1:26657/websocket", + chainId: "juno-1", + factoryAddress: "juno1factory", + routerAddress: "juno1router", + incentivesAddress: "juno1incentives", + oracleAddress: "juno1oracle", + nativeCoinRegistryAddress: "juno1registry", + startHeight: 100, + confirmationDepth: 2, + pollIntervalMs: 1, + batchSize: 20, + dryRun: true, + cursorId: "test", + indexerMode: "realtime", + rangeSize: 5_000, + fetchWindowSize: 250, + fetchConcurrency: 32, + realtimeFetchConcurrency: 8, + rpcTimeoutMs: 10_000, + rpcMaxRetries: 5, + ingestCandlesInline: true, + ingestReserveSnapshotsInline: true, + ingestAggregatesInline: false, + ingestBulkStagingEnabled: false, + priceProviderName: "provider", + priceCacheTtlMs: 300_000, + priceStaleAfterMs: 1_800_000, + priceAllowStale: true, + priceDevMocks: false, + readModelRefreshIntervalMs: 0, + apiPort: 8787, +}; + +class StubIndexer extends Indexer { + constructor(private readonly result: Awaited> & { cursorHeight?: number }) { + super(testConfig); + } + + override async runOnce(): Promise> & { cursorHeight?: number }> { + return this.result; + } +} + +describe("bounded CLI integer parsing", () => { + it("rejects partially numeric values instead of truncating them", () => { + expect(parseNonNegativeInteger("39381355", "to-height")).toBe(39381355); + expect(() => parseNonNegativeInteger("123abc", "to-height")).toThrow(/to-height must be a non-negative integer/i); + expect(() => parseNonNegativeInteger("-1", "to-height")).toThrow(/to-height must be a non-negative integer/i); + }); +}); + +describe("bounded backfill completion", () => { + it("does not mark the range complete until the cursor reaches the requested max height", async () => { + const indexer = new StubIndexer({ processed: 20, head: 200, target: 150, cursorHeight: 120 }); + + await expect(indexer.runUntilHeight(150)).resolves.toMatchObject({ processed: 20, cursorHeight: 120, done: false }); + }); + + it("fails instead of silently succeeding when the confirmed target is below the requested max height", async () => { + const indexer = new StubIndexer({ processed: 0, head: 121, target: 119, cursorHeight: 119 }); + + await expect(indexer.runUntilHeight(150)).rejects.toThrow(/confirmed target 119 is below requested to-height 150/i); + }); +}); + +describe("nextBlockRange", () => { + it("returns an empty range when the confirmed target is behind the next cursor height", () => { + expect(nextBlockRange({ lastHeight: 100, confirmedTarget: 100, batchSize: 20 })).toEqual({ from: 101, to: 100, empty: true }); + }); + + it("limits the next range by batch size", () => { + expect(nextBlockRange({ lastHeight: 100, confirmedTarget: 150, batchSize: 20 })).toEqual({ from: 101, to: 120, empty: false }); + }); + + it("caps the next range at an explicit backfill end height", () => { + expect(nextBlockRange({ lastHeight: 100, confirmedTarget: 150, batchSize: 20, maxHeight: 110 })).toEqual({ from: 101, to: 110, empty: false }); + }); + + it("returns empty after the explicit backfill end height has been reached", () => { + expect(nextBlockRange({ lastHeight: 110, confirmedTarget: 150, batchSize: 20, maxHeight: 110 })).toEqual({ from: 111, to: 110, empty: true }); + }); +}); diff --git a/indexer/test/rpc.test.ts b/indexer/test/rpc.test.ts new file mode 100644 index 000000000..0e26ddd6f --- /dev/null +++ b/indexer/test/rpc.test.ts @@ -0,0 +1,181 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { IndexerMetrics } from "../src/metrics.js"; +import { JunoRestClient, JunoRpcClient } from "../src/rpc.js"; + +afterEach(() => vi.restoreAllMocks()); + +describe("JunoRestClient", () => { + it("queries pair pool state at an explicit historical height", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ + data: { + assets: [ + { info: { native_token: { denom: "ujuno" } }, amount: "123" }, + { info: { token: { contract_addr: "juno1token" } }, amount: "456" }, + ], + total_share: "789", + }, + }), + } as Response); + + const state = await new JunoRestClient("https://lcd.example").poolState("juno1pair", 39381355); + + expect(state).toEqual({ reserves: [{ denom: "ujuno", amount: "123" }, { denom: "juno1token", amount: "456" }], totalShare: "789" }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [url, init] = fetchSpy.mock.calls[0] ?? []; + expect(String(url)).toMatch(/^https:\/\/lcd\.example\/cosmwasm\/wasm\/v1\/contract\/juno1pair\/smart\//); + const encoded = String(url).split("/smart/")[1] ?? ""; + expect(JSON.parse(Buffer.from(decodeURIComponent(encoded), "base64").toString("utf8"))).toEqual({ pool: {} }); + expect((init as RequestInit).headers).toMatchObject({ "x-cosmos-block-height": "39381355" }); + }); + + it("rejects malformed pool responses instead of fabricating reserves", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, json: async () => ({ data: { assets: [] } }) } as Response); + + await expect(new JunoRestClient("https://lcd.example").poolState("juno1pair", 1)).rejects.toThrow(/no reserves/i); + }); +}); + +describe("JunoRpcClient", () => { + it("retries transient RPC statuses and returns the successful response", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce(response(500, "Internal Server Error", {})) + .mockResolvedValueOnce(response(429, "Too Many Requests", {})) + .mockResolvedValueOnce(response(200, "OK", { + result: { sync_info: { latest_block_height: "123", latest_block_hash: "ABC" } }, + })); + + const head = await new JunoRpcClient("https://rpc.example", { timeoutMs: 1_000, maxRetries: 2 }).head(); + + expect(head).toEqual({ height: 123, hash: "ABC" }); + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(fetchSpy.mock.calls.map(([url]) => String(url))).toEqual([ + "https://rpc.example/status", + "https://rpc.example/status", + "https://rpc.example/status", + ]); + }); + + it("honors timeout/retry options while recording RPC metrics", async () => { + const metrics = new IndexerMetrics(); + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce(response(503, "Service Unavailable", {})) + .mockResolvedValueOnce(response(200, "OK", { + result: { sync_info: { latest_block_height: "123", latest_block_hash: "ABC" } }, + })); + + const head = await new JunoRpcClient("https://rpc.example", { timeoutMs: 1_000, maxRetries: 1, metrics }).head(); + + expect(head).toEqual({ height: 123, hash: "ABC" }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + const init = fetchSpy.mock.calls[0]?.[1] as RequestInit | undefined; + expect(init?.signal).toBeInstanceOf(AbortSignal); + const snapshot = metrics.snapshot(); + expect(snapshot.rpcRequestsInFlight).toBe(0); + expect(snapshot.rpcErrors.get("503")).toBe(1); + }); + + it("does not retry permanent RPC statuses and fails clearly", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(response(400, "Bad Request", {})); + + await expect(new JunoRpcClient("https://rpc.example", { maxRetries: 3 }).head()).rejects.toThrow( + "RPC /status failed: 400 Bad Request", + ); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it("fails clearly after transient statuses exhaust retries", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockResolvedValueOnce(response(408, "Request Timeout", {})) + .mockResolvedValueOnce(response(503, "Service Unavailable", {})); + + await expect(new JunoRpcClient("https://rpc.example", { maxRetries: 1 }).head()).rejects.toThrow( + "RPC /status failed: 503 Service Unavailable", + ); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("passes an AbortController signal to RPC fetches for timeout support", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(response(200, "OK", { + result: { sync_info: { latest_block_height: "5", latest_block_hash: "HASH" } }, + })); + + await new JunoRpcClient("https://rpc.example", { timeoutMs: 50, maxRetries: 0 }).head(); + + const init = fetchSpy.mock.calls[0]?.[1] as RequestInit | undefined; + expect(init?.signal).toBeInstanceOf(AbortSignal); + }); + + it("treats aborted RPC requests as transient and retries", async () => { + const abortError = new Error("aborted"); + abortError.name = "AbortError"; + const fetchSpy = vi.spyOn(globalThis, "fetch") + .mockRejectedValueOnce(abortError) + .mockResolvedValueOnce(response(200, "OK", { + result: { sync_info: { latest_block_height: "6", latest_block_hash: "HASH6" } }, + })); + + const head = await new JunoRpcClient("https://rpc.example", { timeoutMs: 50, maxRetries: 1 }).head(); + + expect(head).toEqual({ height: 6, hash: "HASH6" }); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("retries transient failures from block result fetches", async () => { + let blockResultsAttempts = 0; + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/block?height=9") return response(200, "OK", blockResponse(9)); + if (url === "https://rpc.example/block_results?height=9") { + blockResultsAttempts += 1; + if (blockResultsAttempts === 1) return response(503, "Service Unavailable", {}); + return response(200, "OK", { result: { txs_results: [] } }); + } + throw new Error(`unexpected URL ${url}`); + }); + + const block = await new JunoRpcClient("https://rpc.example", { timeoutMs: 1_000, maxRetries: 1 }).block(9); + + expect(block).toMatchObject({ height: 9, hash: "HASH9", txCount: 0, txEvents: [] }); + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(blockResultsAttempts).toBe(2); + }); + + it("records fetched blocks only after both block RPC responses succeed", async () => { + const metrics = new IndexerMetrics(); + vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = String(input); + if (url === "https://rpc.example/block?height=9") return response(200, "OK", blockResponse(9)); + if (url === "https://rpc.example/block_results?height=9") return response(500, "Internal Server Error", {}); + throw new Error(`unexpected URL ${url}`); + }); + + await expect(new JunoRpcClient("https://rpc.example", { maxRetries: 0, metrics }).block(9)).rejects.toThrow( + "RPC /block_results?height=9 failed: 500 Internal Server Error", + ); + + expect(metrics.snapshot().fetchBlocksTotal).toBe(0); + }); +}); + +function blockResponse(height: number): unknown { + return { + result: { + block_id: { hash: `HASH${height}` }, + block: { + header: { time: "2026-01-01T00:00:00Z", last_block_id: { hash: `HASH${height - 1}` } }, + data: { txs: [] }, + }, + }, + }; +} + +function response(status: number, statusText: string, body: unknown): Response { + return { + ok: status >= 200 && status < 300, + status, + statusText, + json: async () => body, + } as Response; +} diff --git a/indexer/test/snapshot-worker.test.ts b/indexer/test/snapshot-worker.test.ts new file mode 100644 index 000000000..27e2436cd --- /dev/null +++ b/indexer/test/snapshot-worker.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_CONTRACTS, type IndexerConfig } from "../src/config.js"; +import { isPermanentSnapshotFailure, SnapshotWorker } from "../src/snapshot-worker.js"; + +type Query = { text: string; values?: unknown[] }; +type JobRow = { id: string; chain_id: string; pair_address: string; height: string; block_time: string; reason: string; status: string; attempts: number }; + +class FakeSnapshotClient { + queries: Query[] = []; + jobRows: JobRow[] = [{ id: "1", chain_id: "juno-1", pair_address: "juno1pair", height: "39381355", block_time: "2026-07-01T03:01:00Z", reason: "touched", status: "leased", attempts: 1 }]; + + async query>(text: string, values?: unknown[]): Promise<{ rows: T[]; rowCount: number }> { + this.queries.push({ text, values }); + if (text.includes("WITH claimable")) return { rows: this.jobRows as T[], rowCount: this.jobRows.length }; + if (text.includes("FROM pools") && text.includes("pair_address")) return { rows: [{ id: "pool-1" }] as T[], rowCount: 1 }; + if (text.includes("INSERT INTO pool_state_snapshots")) return { rows: [] as T[], rowCount: 1 }; + if (text.includes("UPDATE snapshot_jobs")) return { rows: [] as T[], rowCount: 1 }; + return { rows: [] as T[], rowCount: 0 }; + } + + release() {} +} + +class FakeSnapshotPool { + readonly client = new FakeSnapshotClient(); + async connect() { return this.client; } +} + +const config: IndexerConfig = { + databaseUrl: "postgres://test", + rpcUrl: "https://rpc.example", + restUrl: "https://lcd.example", + wsUrl: "wss://rpc.example/websocket", + chainId: "juno-1", + factoryAddress: DEFAULT_CONTRACTS.factory, + routerAddress: DEFAULT_CONTRACTS.router, + incentivesAddress: DEFAULT_CONTRACTS.incentives, + oracleAddress: DEFAULT_CONTRACTS.oracle, + nativeCoinRegistryAddress: DEFAULT_CONTRACTS.nativeCoinRegistry, + startHeight: 11, + confirmationDepth: 2, + pollIntervalMs: 1, + batchSize: 1, + dryRun: false, + cursorId: "astroport-juno-v1", + indexerMode: "realtime", + rangeSize: 5_000, + fetchWindowSize: 250, + fetchConcurrency: 32, + realtimeFetchConcurrency: 8, + rpcTimeoutMs: 10_000, + rpcMaxRetries: 5, + ingestCandlesInline: true, + ingestReserveSnapshotsInline: false, + ingestAggregatesInline: false, + ingestBulkStagingEnabled: false, + priceProviderName: "provider", + priceCacheTtlMs: 300_000, + priceStaleAfterMs: 1_800_000, + priceAllowStale: true, + priceDevMocks: false, + readModelRefreshIntervalMs: 0, + apiPort: 8787, +}; + +describe("SnapshotWorker", () => { + it("processes a claimed job by querying LCD at height, writing a snapshot, and marking success", async () => { + const pool = new FakeSnapshotPool(); + const rest = { + poolState: async (pairAddress: string, height?: number) => { + expect(pairAddress).toBe("juno1pair"); + expect(height).toBe(39381355); + return { reserves: [{ denom: "ujuno", amount: "123" }], totalShare: "456" }; + }, + }; + + await expect(new SnapshotWorker(config, pool as never, rest, { batchSize: 10, leaseSeconds: 30, maxAttempts: 3 }).processBatch()).resolves.toBe(1); + + const claim = pool.client.queries.find((query) => query.text.includes("FOR UPDATE SKIP LOCKED")); + expect(claim?.values).toEqual(["juno-1", 10, "30 seconds", 3]); + const snapshot = pool.client.queries.find((query) => query.text.includes("INSERT INTO pool_state_snapshots")); + expect(snapshot?.values).toEqual(["pool-1", 39381355, "2026-07-01T03:01:00Z", JSON.stringify([{ denom: "ujuno", amount: "123" }]), "456", "lcd"]); + expect(pool.client.queries.some((query) => query.text.includes("status = 'succeeded'"))).toBe(true); + }); + + it("retries transient LCD failures by returning the job to pending", async () => { + const pool = new FakeSnapshotPool(); + const rest = { poolState: async () => { throw new Error("LCD smart query failed: 500 unavailable"); } }; + + await expect(new SnapshotWorker(config, pool as never, rest, { maxAttempts: 5 }).processBatch()).resolves.toBe(1); + + const failure = pool.client.queries.find((query) => query.text.includes("last_error = $4")); + expect(failure?.values).toEqual(["1", 1, false, "LCD smart query failed: 500 unavailable", 5]); + }); + + it("marks permanent failures without retrying", async () => { + const pool = new FakeSnapshotPool(); + const rest = { poolState: async () => { throw new Error("LCD smart query failed: 404 Not Found"); } }; + + await expect(new SnapshotWorker(config, pool as never, rest, { maxAttempts: 5 }).processBatch()).resolves.toBe(1); + + const failure = pool.client.queries.find((query) => query.text.includes("last_error = $4")); + expect(failure?.values).toEqual(["1", 1, true, "LCD smart query failed: 404 Not Found", 5]); + expect(isPermanentSnapshotFailure(new Error("LCD smart query failed: 404 Not Found"))).toBe(true); + expect(isPermanentSnapshotFailure(new Error("LCD smart query failed: 429 Too Many Requests"))).toBe(false); + }); +}); diff --git a/indexer/tsconfig.json b/indexer/tsconfig.json new file mode 100644 index 000000000..0ad465c2f --- /dev/null +++ b/indexer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/integration-tests/tests/incentives_fake_pair_rejected.rs b/integration-tests/tests/incentives_fake_pair_rejected.rs index b94ca812b..fe1fddfa2 100644 --- a/integration-tests/tests/incentives_fake_pair_rejected.rs +++ b/integration-tests/tests/incentives_fake_pair_rejected.rs @@ -36,8 +36,8 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ - coin, to_json_binary, Addr, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, - StdError, StdResult, Timestamp, Uint128, + coin, to_json_binary, Addr, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, Timestamp, Uint128, }; use cw_storage_plus::Item; @@ -75,7 +75,13 @@ fn incentivize_rejects_fake_pair_impersonating_real_asset_infos() { // Stand up the real (UJUNO, MOCK_USDC) pair so the factory has an // entry for that asset_info pair — the attacker will try to // impersonate it. - let real_pair = create_pair(&mut app, &handles.factory, &handles.deployer, UJUNO, MOCK_USDC); + let real_pair = create_pair( + &mut app, + &handles.factory, + &handles.deployer, + UJUNO, + MOCK_USDC, + ); let real_lp_denom = lp_denom_of(&app, &handles.factory, UJUNO, MOCK_USDC); // Deploy the fake-pair contract. Its sole purpose is to respond to @@ -85,8 +91,12 @@ fn incentivize_rejects_fake_pair_impersonating_real_asset_infos() { let fake_pair = instantiate_fake_pair( &mut app, vec![ - AssetInfo::NativeToken { denom: UJUNO.to_string() }, - AssetInfo::NativeToken { denom: MOCK_USDC.to_string() }, + AssetInfo::NativeToken { + denom: UJUNO.to_string(), + }, + AssetInfo::NativeToken { + denom: MOCK_USDC.to_string(), + }, ], ); let fake_denom = format!("factory/{fake_pair}/astroport/share"); @@ -136,8 +146,14 @@ fn incentivize_rejects_fake_pair_impersonating_real_asset_infos() { ); // Make sure the error message names both contenders so on-chain // attribution can attribute the spoof. - assert!(msg.contains(real_pair.as_str()), "error names the real pair address: {msg}"); - assert!(msg.contains(fake_pair.as_str()), "error names the fake (claimed) pair address: {msg}"); + assert!( + msg.contains(real_pair.as_str()), + "error names the real pair address: {msg}" + ); + assert!( + msg.contains(fake_pair.as_str()), + "error names the fake (claimed) pair address: {msg}" + ); // Belt-and-braces: the real pair's LP denom must still be acceptable // (proving the new gate doesn't false-positive on legitimate pairs). @@ -174,7 +190,9 @@ fn incentivize_rejects_fake_pair_with_unregistered_asset_infos() { let fake_pair = instantiate_fake_pair( &mut app, vec![ - AssetInfo::NativeToken { denom: UJUNO.to_string() }, + AssetInfo::NativeToken { + denom: UJUNO.to_string(), + }, AssetInfo::NativeToken { denom: "ibc/no-such-pair-token".to_string(), }, @@ -210,7 +228,9 @@ fn incentivize_rejects_fake_pair_with_unregistered_asset_infos() { }, &[coin(REWARD_AMOUNT, UJUNO)], ) - .expect_err("Incentivize against a fake pair with unregistered asset_infos must be rejected by rc4"); + .expect_err( + "Incentivize against a fake pair with unregistered asset_infos must be rejected by rc4", + ); let msg = err.root_cause().to_string(); assert!( @@ -223,10 +243,20 @@ fn incentivize_rejects_fake_pair_with_unregistered_asset_infos() { // helpers // ===================================================================== -fn create_pair(app: &mut TestApp, factory: &Addr, deployer: &Addr, denom_a: &str, denom_b: &str) -> Addr { +fn create_pair( + app: &mut TestApp, + factory: &Addr, + deployer: &Addr, + denom_a: &str, + denom_b: &str, +) -> Addr { let asset_infos = vec![ - AssetInfo::NativeToken { denom: denom_a.to_string() }, - AssetInfo::NativeToken { denom: denom_b.to_string() }, + AssetInfo::NativeToken { + denom: denom_a.to_string(), + }, + AssetInfo::NativeToken { + denom: denom_b.to_string(), + }, ]; app.execute_contract( deployer.clone(), @@ -253,8 +283,12 @@ fn lp_denom_of(app: &TestApp, factory: &Addr, denom_a: &str, denom_b: &str) -> S factory.clone(), &FactoryQueryMsg::Pair { asset_infos: vec![ - AssetInfo::NativeToken { denom: denom_a.to_string() }, - AssetInfo::NativeToken { denom: denom_b.to_string() }, + AssetInfo::NativeToken { + denom: denom_a.to_string(), + }, + AssetInfo::NativeToken { + denom: denom_b.to_string(), + }, ], }, ) diff --git a/integration-tests/tests/incentives_migrate_legacy_config.rs b/integration-tests/tests/incentives_migrate_legacy_config.rs index d00382324..943888429 100644 --- a/integration-tests/tests/incentives_migrate_legacy_config.rs +++ b/integration-tests/tests/incentives_migrate_legacy_config.rs @@ -181,18 +181,25 @@ fn migrate_from_upstream_unsupported_version_rejected() { // Seed legacy state: cw2 says "astroport-incentives 1.3.0" (note: the // fork's CARGO_PKG_NAME match is just "astroport-incentives" — the // crates.io-published `crates.io:` prefix is exercised in test 2). - write_legacy_cw2(&mut app, &incentives, FORK_CONTRACT_NAME, UPSTREAM_VERSION_1_3_0); + write_legacy_cw2( + &mut app, + &incentives, + FORK_CONTRACT_NAME, + UPSTREAM_VERSION_1_3_0, + ); write_legacy_config_json(&mut app, &incentives); // Snapshot the legacy bytes so we can prove no partial write. - let cw2_before = read_raw(&app, &incentives, CW2_CONTRACT_INFO_KEY) - .expect("cw2 contract_info seeded"); + let cw2_before = + read_raw(&app, &incentives, CW2_CONTRACT_INFO_KEY).expect("cw2 contract_info seeded"); let config_before = read_raw(&app, &incentives, CONFIG_KEY).expect("legacy config seeded"); // Migrate to the same code_id (real-world this would be a new wasm // upload, but for the rejection path the code identity doesn't // matter — the guard fires before any contract logic runs). - let deployer = app.api().addr_make(astroport_juno_integration_tests::DEPLOYER); + let deployer = app + .api() + .addr_make(astroport_juno_integration_tests::DEPLOYER); let err = app .migrate_contract(deployer, incentives.clone(), &Empty {}, code_id) .expect_err( @@ -260,7 +267,9 @@ fn migrate_from_upstream_contract_name_rejected() { ); write_legacy_config_json(&mut app, &incentives); - let deployer = app.api().addr_make(astroport_juno_integration_tests::DEPLOYER); + let deployer = app + .api() + .addr_make(astroport_juno_integration_tests::DEPLOYER); let err = app .migrate_contract(deployer, incentives.clone(), &Empty {}, code_id) .expect_err( @@ -308,7 +317,9 @@ fn migrate_from_current_fork_version_rejected() { assert_eq!(cw2.contract, FORK_CONTRACT_NAME); assert_eq!(cw2.version, FORK_CONTRACT_VERSION); - let deployer = app.api().addr_make(astroport_juno_integration_tests::DEPLOYER); + let deployer = app + .api() + .addr_make(astroport_juno_integration_tests::DEPLOYER); let err = app .migrate_contract(deployer, incentives.clone(), &Empty {}, code_id) .expect_err( diff --git a/integration-tests/tests/incentives_stuck_funds_recovered.rs b/integration-tests/tests/incentives_stuck_funds_recovered.rs index 73defd734..ab4ea9255 100644 --- a/integration-tests/tests/incentives_stuck_funds_recovered.rs +++ b/integration-tests/tests/incentives_stuck_funds_recovered.rs @@ -606,12 +606,7 @@ mod mock_cw20 { } } - fn transfer_inner( - state: &mut State, - from: &str, - to: &str, - amount: Uint128, - ) -> StdResult<()> { + fn transfer_inner(state: &mut State, from: &str, to: &str, amount: Uint128) -> StdResult<()> { // Subtract from sender. let from_balance = state .balances @@ -653,5 +648,4 @@ mod mock_cw20 { .map_err(StdError::overflow)?; Ok(()) } - } diff --git a/integration-tests/tests/incentives_zero_stake_orphans.rs b/integration-tests/tests/incentives_zero_stake_orphans.rs new file mode 100644 index 000000000..9d6bc0b06 --- /dev/null +++ b/integration-tests/tests/incentives_zero_stake_orphans.rs @@ -0,0 +1,351 @@ +//! Regression tests for zero-stake incentives accrual. +//! +//! Rewards emitted while no LP is staked must not be assigned to the first +//! depositor. They are protocol orphans recoverable by the DAO, not user yield. + +use cosmwasm_std::{coin, Addr, Timestamp, Uint128}; + +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::factory::{ExecuteMsg as FactoryExecuteMsg, PairType, QueryMsg as FactoryQueryMsg}; +use astroport::incentives::{ExecuteMsg as IncentivesExecuteMsg, InputSchedule, EPOCHS_START}; +use astroport::pair::ExecuteMsg as PairExecuteMsg; +use astroport_test::cw_multi_test::{BankSudo, Executor, SudoMsg}; + +use astroport_juno_integration_tests::{ + balance_of, deploy_incentives_addon, deploy_keep_set, fund, mock_app, KeepSetHandles, TestApp, + MOCK_USDC, UJUNO, +}; + +const ALICE: &str = "alice"; +const FUNDER: &str = "funder"; +const LP_SEED: u128 = 100_000_000_000; +const INTERNAL_REWARD_FUND_AMOUNT: u128 = 10_000_000; +const TOKENS_PER_SECOND: u128 = 100; +const EMPTY_SECONDS: u64 = 10; +const STAKED_SECONDS: u64 = 1; +const PROJECT_REWARD: &str = "factory/juno1projectaddr/ZERO"; +const EXTERNAL_REWARD_AMOUNT: u128 = 100_000_000; + +#[test] +fn internal_emissions_before_first_stake_are_orphaned_not_paid_to_first_depositor() { + let mut app = mock_app(); + app.update_block(|b| { + b.time = Timestamp::from_seconds(EPOCHS_START + 86400); + b.height += 1; + }); + + let handles = deploy_keep_set(&mut app).unwrap(); + let inc = deploy_incentives_addon( + &mut app, + &handles, + AssetInfo::NativeToken { + denom: UJUNO.to_string(), + }, + None, + ) + .unwrap(); + + let pair = create_pair(&mut app, &handles, UJUNO, MOCK_USDC); + let lp_denom = lp_denom_of(&mut app, &handles, UJUNO, MOCK_USDC); + + let alice = app.api().addr_make(ALICE); + fund( + &mut app, + &alice, + vec![coin(LP_SEED, UJUNO), coin(LP_SEED, MOCK_USDC)], + ) + .unwrap(); + provide_liquidity(&mut app, &pair, &alice, LP_SEED, LP_SEED); + let alice_lp = balance_of(&app, &alice, &lp_denom); + assert!(alice_lp > Uint128::zero(), "Alice received LP tokens"); + + app.execute_contract( + handles.deployer.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::SetupPools { + pools: vec![(lp_denom.clone(), Uint128::new(1))], + }, + &[], + ) + .unwrap(); + app.execute_contract( + handles.deployer.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::SetTokensPerSecond { + amount: Uint128::new(TOKENS_PER_SECOND), + }, + &[], + ) + .unwrap(); + fund( + &mut app, + &inc.incentives, + vec![coin(INTERNAL_REWARD_FUND_AMOUNT, UJUNO)], + ) + .unwrap(); + + // Emissions run while nobody is staked. + app.update_block(|b| { + b.time = b.time.plus_seconds(EMPTY_SECONDS); + b.height += 1; + }); + + app.execute_contract( + alice.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::Deposit { recipient: None }, + &[coin(alice_lp.u128(), lp_denom.clone())], + ) + .unwrap(); + + // Let one second accrue with Alice actually staked. She may receive this + // second, but not the EMPTY_SECONDS that elapsed before any stake existed. + app.update_block(|b| { + b.time = b.time.plus_seconds(STAKED_SECONDS); + b.height += 1; + }); + + let alice_ujuno_before = balance_of(&app, &alice, UJUNO); + app.execute_contract( + alice.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::ClaimRewards { + lp_tokens: vec![lp_denom.clone()], + }, + &[], + ) + .unwrap(); + let alice_ujuno_after = balance_of(&app, &alice, UJUNO); + let received = alice_ujuno_after - alice_ujuno_before; + let max_staked_period_reward = Uint128::new(STAKED_SECONDS as u128 * TOKENS_PER_SECOND + 1); + assert!( + received <= max_staked_period_reward, + "first depositor received empty-period emissions: got {received}, expected at most {max_staked_period_reward}" + ); + + let dao_before = balance_of(&app, &handles.deployer, UJUNO); + app.execute_contract( + handles.deployer.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::ClaimOrphanedRewards { + limit: None, + receiver: handles.deployer.to_string(), + }, + &[], + ) + .expect("DAO can recover zero-stake internal emissions as orphaned rewards"); + let dao_after = balance_of(&app, &handles.deployer, UJUNO); + assert!( + dao_after > dao_before, + "zero-stake internal emissions should be recoverable by the DAO" + ); +} + +#[test] +fn external_incentives_before_first_stake_are_orphaned_not_paid_to_first_depositor() { + let mut app = mock_app(); + app.update_block(|b| { + b.time = Timestamp::from_seconds(EPOCHS_START + 86400); + b.height += 1; + }); + + let handles = deploy_keep_set(&mut app).unwrap(); + let inc = deploy_incentives_addon( + &mut app, + &handles, + AssetInfo::NativeToken { + denom: UJUNO.to_string(), + }, + None, + ) + .unwrap(); + + let pair = create_pair(&mut app, &handles, UJUNO, MOCK_USDC); + let lp_denom = lp_denom_of(&mut app, &handles, UJUNO, MOCK_USDC); + + let alice = app.api().addr_make(ALICE); + fund( + &mut app, + &alice, + vec![coin(LP_SEED, UJUNO), coin(LP_SEED, MOCK_USDC)], + ) + .unwrap(); + provide_liquidity(&mut app, &pair, &alice, LP_SEED, LP_SEED); + let alice_lp = balance_of(&app, &alice, &lp_denom); + + let funder = app.api().addr_make(FUNDER); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: funder.to_string(), + amount: vec![coin(EXTERNAL_REWARD_AMOUNT, PROJECT_REWARD)], + })) + .unwrap(); + + app.execute_contract( + funder.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::Incentivize { + lp_token: lp_denom.clone(), + schedule: InputSchedule { + reward: Asset { + info: AssetInfo::NativeToken { + denom: PROJECT_REWARD.to_string(), + }, + amount: Uint128::new(EXTERNAL_REWARD_AMOUNT), + }, + duration_periods: 1, + }, + }, + &[coin(EXTERNAL_REWARD_AMOUNT, PROJECT_REWARD)], + ) + .expect("Incentivize succeeds while no LP is staked"); + + // Advance into the active external schedule while nobody is staked. + app.update_block(|b| { + b.time = b.time.plus_seconds(7 * 86400); + b.height += 1; + }); + + app.execute_contract( + alice.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::Deposit { recipient: None }, + &[coin(alice_lp.u128(), lp_denom.clone())], + ) + .unwrap(); + + app.update_block(|b| { + b.time = b.time.plus_seconds(STAKED_SECONDS); + b.height += 1; + }); + + let alice_reward_before = balance_of(&app, &alice, PROJECT_REWARD); + app.execute_contract( + alice.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::ClaimRewards { + lp_tokens: vec![lp_denom.clone()], + }, + &[], + ) + .unwrap(); + let alice_reward_after = balance_of(&app, &alice, PROJECT_REWARD); + let received = alice_reward_after - alice_reward_before; + assert!( + received <= Uint128::new(100), + "first depositor received zero-stake external emissions: got {received}" + ); + + let dao_before = balance_of(&app, &handles.deployer, PROJECT_REWARD); + app.execute_contract( + handles.deployer.clone(), + inc.incentives.clone(), + &IncentivesExecuteMsg::ClaimOrphanedRewards { + limit: None, + receiver: handles.deployer.to_string(), + }, + &[], + ) + .expect("DAO can recover zero-stake external emissions as orphaned rewards"); + let dao_after = balance_of(&app, &handles.deployer, PROJECT_REWARD); + assert!( + dao_after > dao_before, + "zero-stake external emissions should be recoverable by the DAO" + ); +} + +// ===================================================================== +// helpers (local to this test target) +// ===================================================================== + +fn create_pair(app: &mut TestApp, handles: &KeepSetHandles, denom_a: &str, denom_b: &str) -> Addr { + let asset_infos = vec![ + AssetInfo::NativeToken { + denom: denom_a.to_string(), + }, + AssetInfo::NativeToken { + denom: denom_b.to_string(), + }, + ]; + app.execute_contract( + handles.deployer.clone(), + handles.factory.clone(), + &FactoryExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + &[], + ) + .unwrap(); + let info: PairInfo = app + .wrap() + .query_wasm_smart( + handles.factory.clone(), + &FactoryQueryMsg::Pair { asset_infos }, + ) + .unwrap(); + info.contract_addr +} + +fn lp_denom_of( + app: &mut TestApp, + handles: &KeepSetHandles, + denom_a: &str, + denom_b: &str, +) -> String { + let info: PairInfo = app + .wrap() + .query_wasm_smart( + handles.factory.clone(), + &FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: denom_a.to_string(), + }, + AssetInfo::NativeToken { + denom: denom_b.to_string(), + }, + ], + }, + ) + .unwrap(); + info.liquidity_token +} + +fn provide_liquidity( + app: &mut TestApp, + pair: &Addr, + sender: &Addr, + a_amount: u128, + b_amount: u128, +) { + let assets = vec![ + Asset { + info: AssetInfo::NativeToken { + denom: UJUNO.to_string(), + }, + amount: Uint128::new(a_amount), + }, + Asset { + info: AssetInfo::NativeToken { + denom: MOCK_USDC.to_string(), + }, + amount: Uint128::new(b_amount), + }, + ]; + let mut funds = vec![coin(a_amount, UJUNO), coin(b_amount, MOCK_USDC)]; + funds.sort_by(|a, b| a.denom.cmp(&b.denom)); + app.execute_contract( + sender.clone(), + pair.clone(), + &PairExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance: None, + auto_stake: None, + receiver: None, + min_lp_to_receive: None, + }, + &funds, + ) + .unwrap(); +} diff --git a/packages/astroport/src/incentives.rs b/packages/astroport/src/incentives.rs index 9e671fc4d..dfdc14473 100644 --- a/packages/astroport/src/incentives.rs +++ b/packages/astroport/src/incentives.rs @@ -1,8 +1,8 @@ use std::hash::{Hash, Hasher}; use std::ops::RangeInclusive; -use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_schema::serde::{de::Error as _, Deserialize as _, Deserializer}; +use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin, Decimal256, Env, StdError, StdResult, Uint128}; use crate::asset::{Asset, AssetInfo}; @@ -341,8 +341,8 @@ fn deserialize_generator_controller_update<'de, D>( where D: Deserializer<'de>, { - let opt = Option::::deserialize(deserializer) - .map_err(D::Error::custom)?; + let opt = + Option::::deserialize(deserializer).map_err(D::Error::custom)?; Ok(opt.unwrap_or_default()) } diff --git a/packages/astroport_juno_types/tests/wire_drift.rs b/packages/astroport_juno_types/tests/wire_drift.rs index 6ffe7b8e0..154844e3a 100644 --- a/packages/astroport_juno_types/tests/wire_drift.rs +++ b/packages/astroport_juno_types/tests/wire_drift.rs @@ -508,8 +508,8 @@ fn pair_xyk_pool_params_unpause_omitted_default() { assert_eq!(shim.track_asset_balances, Some(true)); assert_eq!(shim.pool_unpause_at, None); - let upstream: astroport::pair::XYKPoolParams = serde_json::from_str(legacy_json) - .expect("upstream accepts legacy XYKPoolParams JSON"); + let upstream: astroport::pair::XYKPoolParams = + serde_json::from_str(legacy_json).expect("upstream accepts legacy XYKPoolParams JSON"); assert_eq!(upstream.track_asset_balances, Some(true)); assert_eq!(upstream.pool_unpause_at, None); } @@ -646,8 +646,8 @@ fn pair_execute_withdraw_liquidity_assets_omitted_default() { other => panic!("expected WithdrawLiquidity, got {other:?}"), } - let upstream: astroport::pair::ExecuteMsg = serde_json::from_str(legacy_json) - .expect("upstream accepts legacy WithdrawLiquidity JSON"); + let upstream: astroport::pair::ExecuteMsg = + serde_json::from_str(legacy_json).expect("upstream accepts legacy WithdrawLiquidity JSON"); match upstream { astroport::pair::ExecuteMsg::WithdrawLiquidity { assets, @@ -969,32 +969,48 @@ fn pair_config_response_bidir() { fn pair_cumulative_prices_response_bidir() { let assets_juno = vec![ juno::asset::Asset { - info: juno::asset::AssetInfo::NativeToken { denom: "ujuno".to_string() }, + info: juno::asset::AssetInfo::NativeToken { + denom: "ujuno".to_string(), + }, amount: Uint128::new(1_000_000), }, juno::asset::Asset { - info: juno::asset::AssetInfo::NativeToken { denom: "ibc/USDC".to_string() }, + info: juno::asset::AssetInfo::NativeToken { + denom: "ibc/USDC".to_string(), + }, amount: Uint128::new(500_000), }, ]; let assets_up = vec![ astroport::asset::Asset { - info: astroport::asset::AssetInfo::NativeToken { denom: "ujuno".to_string() }, + info: astroport::asset::AssetInfo::NativeToken { + denom: "ujuno".to_string(), + }, amount: Uint128::new(1_000_000), }, astroport::asset::Asset { - info: astroport::asset::AssetInfo::NativeToken { denom: "ibc/USDC".to_string() }, + info: astroport::asset::AssetInfo::NativeToken { + denom: "ibc/USDC".to_string(), + }, amount: Uint128::new(500_000), }, ]; let prices_juno = vec![( - juno::asset::AssetInfo::NativeToken { denom: "ujuno".to_string() }, - juno::asset::AssetInfo::NativeToken { denom: "ibc/USDC".to_string() }, + juno::asset::AssetInfo::NativeToken { + denom: "ujuno".to_string(), + }, + juno::asset::AssetInfo::NativeToken { + denom: "ibc/USDC".to_string(), + }, Uint128::new(987_654_321), )]; let prices_up = vec![( - astroport::asset::AssetInfo::NativeToken { denom: "ujuno".to_string() }, - astroport::asset::AssetInfo::NativeToken { denom: "ibc/USDC".to_string() }, + astroport::asset::AssetInfo::NativeToken { + denom: "ujuno".to_string(), + }, + astroport::asset::AssetInfo::NativeToken { + denom: "ibc/USDC".to_string(), + }, Uint128::new(987_654_321), )]; bidir_roundtrip( diff --git a/planning/00-overview.md b/planning/00-overview.md index 7d7c6f4cc..23a230679 100644 --- a/planning/00-overview.md +++ b/planning/00-overview.md @@ -78,6 +78,31 @@ Three diffs to the AI audit; one mechanical (A), two functional (B, C). See | `10-open-questions.md` | Running list. | | `11-incentives-and-gauges.md` | (P2.5) Re-introduce `astroport-incentives`; bind to DAO DAO gauge for community-voted emissions; permissionless external incentives. | | `12-incentives-strip-decisions.md` | (P2.5) ADR D6 — per-decision rationale for the rc1→rc2 strip (vesting, cw20 axes, naming, MAX_REWARD_TOKENS, generator_controller, bech32 test mode). | +| `13-scope-guard-verification-2026-06-28.md` | Scope guard verification notes for the Juno v1 contract set. | +| `14-schema-scope-guard-2026-06-28.md` | Schema pruning and schema-set guard notes. | +| `15-juno-v1-guard-readiness-2026-06-29.md` | Readiness check notes for the Juno v1 guards. | +| `16-ci-artifact-guard-2026-06-29.md` | CI artifact-set guard notes for optimized wasm output. | +| `17-frontend-schema-surface-2026-06-29.md` | Frontend integration map generated from committed v1 JSON schemas. | +| `18-deployment-template-guard-2026-06-29.md` | Testnet deployment config template and schema-derived guard. | +| `19-ci-deployment-template-guard-2026-06-29.md` | CI wiring for the deployment template guard. | +| `20-ci-wiring-guard-2026-06-29.md` | Dependency-free guard that verifies the Juno v1 CI guard ordering. | +| `21-deployment-fill-script-2026-06-29.md` | Renderer for turning the uni-7 deployment template into a concrete config once code IDs/addresses are known. | +| `22-deployment-readme-2026-06-29.md` | Operator/frontend handoff README for filling and validating uni-7 deployment config values. | +| `23-tx-json-extraction-helper-2026-06-29.md` | Helper for extracting deployment `--set` values from `junod -o json` store/instantiate tx responses. | +| `24-tx-extractor-fixture-guard-2026-06-29.md` | CI fixture guard for the tx JSON extraction helper and launch-guard ordering. | +| `25-deployment-command-bundle-2026-06-29.md` | Final deployment command bundler that combines tx-derived values with manual operator values and validates rendered config. | +| `26-operator-tx-checklist-guard-2026-06-29.md` | Operator tx filename checklist plus CI guard for the uni-7 deployment handoff. | +| `27-dry-run-tx-fixtures-2026-06-29.md` | Dry-run tx fixture generator and rehearsal guard for the uni-7 handoff. | +| `28-dry-run-ci-wiring-2026-06-29.md` | CI wiring for the dry-run tx rehearsal guard. | +| `29-deployment-gitignore-guard-2026-06-29.md` | Gitignore guard that keeps local tx JSON and rendered deployment configs out of commits. | +| `30-frontend-config-guard-2026-06-29.md` | Frontend handoff guard that verifies rendered config address wiring and first XYK pair template without chain access. | +| `31-dry-run-frontend-validation-2026-06-29.md` | Dry-run deployment rehearsal now validates the temp rendered config with both deployment and frontend guards. | +| `32-frontend-types-handoff-2026-06-29.md` | Generated TypeScript frontend handoff type and CI guard tied to the deployment template. | +| `33-frontend-example-guard-2026-06-29.md` | TypeScript frontend consumer example plus CI guard for the generated handoff type. | +| `34-frontend-readme-consumption-2026-06-29.md` | Frontend README snippet showing how to import rendered JSON with the generated handoff type. | +| `35-deployment-readme-guard-2026-06-29.md` | Deployment README guard that keeps operator/frontend handoff docs aligned with helper scripts. | +| `36-frontend-handoff-sync-guard-2026-06-29.md` | Frontend address key sync guard across template, TypeScript, example, README, and CI. | +| `37-frontend-release-checklist-guard-2026-06-29.md` | Frontend release checklist and guard for copying rendered deployment files into the UI repo. | Files marked "(P*)" are stubs until that phase begins. diff --git a/planning/01-strip-list.md b/planning/01-strip-list.md index f9e6686e2..6bfac7f56 100644 --- a/planning/01-strip-list.md +++ b/planning/01-strip-list.md @@ -8,13 +8,14 @@ table is wrong — fix it. | Path | Role | Notes | |---|---|---| -| `contracts/factory` | Pool creation + registry | `whitelist_code_id` non-optional — see whitelist note below. `generator_address: None` in v1 (no incentives). | +| `contracts/factory` | Pool creation + registry | `whitelist_code_id` non-optional — see whitelist note below. `generator_address` points at `contracts/tokenomics/incentives` once incentives are instantiated. | | `contracts/pair` | XYK constant-product AMM | Receives `pool_unpause_at` patch in P2. LP token is TokenFactory-native (no cw20 LP code path in v5.13.1). | | `contracts/router` | Multi-hop swap composition | XYK-only routing in v1. Dev-deps on `pair_concentrated` will be removed in P0. | | `contracts/whitelist` | cw1-style permissioned-pair gate | Neutron-stripped in P0 (drops `neutron-sdk`, `NeutronMsg`, `sudo` entry, `src/ibc.rs`). See `03-whitelist-decision.md`. | | `contracts/periphery/native_coin_registry` | Native-denom precision oracle | Required by factory; v1 seeds with `ujuno` + canonical IBC denoms. | | `contracts/periphery/oracle` | Per-pair TWAP | Uploaded but not auto-instantiated. UI uses for price charts. 1-day hardcoded window. | | `contracts/periphery/tokenfactory_tracker` | TF snapshot tracker | Uploaded, dormant in v1. Wakes up when a pair sets `track_asset_balances: true`. | +| `contracts/tokenomics/incentives` | LP reward distributor | Re-added in P2.5 for DAO-funded `ujuno` emissions plus permissionless external cw20/native rewards; no DEX token, no vesting, no staking. See `11-incentives-and-gauges.md` and ADR D6. | | `packages/astroport` | Wire-type + helper crate | Most modules kept; chain-specific modules removed (see "Pruned modules" below). | | `packages/astroport_test` | cw-multi-test harness | Injective feature branches stripped. | @@ -39,7 +40,7 @@ audit-delta cycle, not a re-fork from upstream. | `contracts/pair_transmuter` | 1:1 constant-sum. Niche. Defer indefinitely. | | `contracts/pair_xyk_sale_tax` | XYK with sale tax. cw-abc graduation handles meme-launch flow orthogonally. | | `contracts/pair_concentrated_sale_tax` | PCL with sale tax. Same reason. | -| `contracts/tokenomics/` (entire subtree) | No DEX token in v1 — no incentives, maker fee collector, xASTRO staking, vesting, or xastro_token. | +| `contracts/tokenomics/maker`, `contracts/tokenomics/staking`, `contracts/tokenomics/vesting`, `contracts/tokenomics/xastro_token` | No DEX token in v1 — no maker fee collector, xASTRO staking, vesting, or xASTRO token. `contracts/tokenomics/incentives` is the only tokenomics contract re-added for Juno v1. | | `contracts/periphery/astro_converter` | Terra-specific cw20→TF converter. | | `contracts/periphery/astro_converter_neutron` | Neutron outpost of above. | | `e2e/` | TypeScript e2e harness bound to localterra-1 + localneutron-1 via feather.js. Juno-incompatible without a full rewrite. | @@ -82,7 +83,8 @@ astroport_native_coin_registry.wasm astroport_oracle.wasm astroport_tokenfactory_tracker.wasm astroport_whitelist.wasm +astroport_incentives.wasm ``` -7 wasms. Each must pass `cosmwasm-check --available-capabilities staking,cosmwasm_1_1,cosmwasm_2_0,iterator,stargate` +8 wasms. Each must pass `cosmwasm-check --available-capabilities staking,cosmwasm_1_1,cosmwasm_2_0,iterator,stargate` (no `neutron` capability). diff --git a/planning/04-incentives-types-decision.md b/planning/04-incentives-types-decision.md index 45da23b2c..6629b6b48 100644 --- a/planning/04-incentives-types-decision.md +++ b/planning/04-incentives-types-decision.md @@ -1,7 +1,9 @@ # 04 — Incentives types-only retention (ADR D1) -**Status:** decided 2026-05-13. Keep `packages/astroport/src/incentives.rs` -as a types-only module. Do not refactor the factory's import of it. +**Status:** superseded by P2.5 / ADR D6. This ADR records the earlier rc0/rc1 +reason for keeping `packages/astroport/src/incentives.rs` while the incentives +contract was stripped. Juno v1 now ships a stripped `contracts/tokenomics/incentives` +contract; see `11-incentives-and-gauges.md` and `12-incentives-strip-decisions.md`. ## Problem @@ -10,9 +12,10 @@ as a types-only module. Do not refactor the factory's import of it. this message when a pair is deactivated and the factory has a `generator_address: Some(_)` configured. -In v1 we ship `generator_address: None` (no incentives, no DEX token, see -`memory/juno-defi-direction.md`). The code path that constructs the -`DeactivatePool` message is unreachable. +In rc0/rc1 we shipped `generator_address: None` (no incentives, no DEX token, +see `memory/juno-defi-direction.md`). The code path that constructs the +`DeactivatePool` message was unreachable at that boundary. In P2.5, incentives +returned to v1 scope without adding a DEX token. But: `packages/astroport/src/incentives.rs` defines the type. If we delete that module, the factory no longer compiles. @@ -42,36 +45,39 @@ Two sub-options: Both larger diffs than (a) and worse on every axis. -## Decision +## Decision at rc0/rc1 Option (a). Keep `packages/astroport/src/incentives.rs` exactly as upstream. Keep `pub mod incentives;` in `src/lib.rs`. The factory's import line at `contracts/factory/src/contract.rs:19` stays untouched. -## What changes in the strip +## What changed in the rc0/rc1 strip -- The `contracts/tokenomics/incentives` *contract* is deleted (it implements - the incentives state machine; we don't ship it). +- The `contracts/tokenomics/incentives` *contract* was deleted at rc0/rc1 (it + implements the incentives state machine; we did not ship it at that boundary). - The `astroport::incentives` *types module* in `packages/astroport` stays. +P2.5 supersedes the first bullet: `contracts/tokenomics/incentives` is now a +workspace member again, with Juno-specific strips documented in ADR D6. + The factory imports `astroport::incentives::ExecuteMsg::DeactivatePool` — -the *type*, not the contract. Type stays; contract goes. +the *type*, not the contract binary. Type stays; the stripped incentives +contract now ships separately as a v1 workspace member. ## How to apply this in audit narrative In `planning/07-audit-scope.md`, the audit-house brief should state: > The `astroport::incentives` module in `packages/astroport` is retained as -> a wire-type declaration only. The corresponding `tokenomics/incentives` -> contract is not shipped. The factory's single import-site of this type is -> in a `generator_address: Some(_)` branch that is never reached in v1 -> deployments (factory is instantiated with `generator_address: None`). -> Treat the import as dead code; treat the type module as a JSON schema -> artifact, not a contract surface. - -## Future shape - -When v2 brings incentives: ship a new `astroport-incentives` contract built -from upstream Astroport's reference impl, instantiate it, set -`factory.generator_address: Some(addr)`. Factory's existing -`DeactivatePool` import-site activates with no factory change required. +> the canonical wire-type surface for both the factory deactivation hook and +> the shipped `contracts/tokenomics/incentives` contract. In rc0/rc1 the +> module was types-only because the contract was out of scope; P2.5 re-added +> the stripped incentives contract. Audit the type module together with the +> incentives contract and confirm factory `DeactivatePool` messages still +> match the shipped execute schema. + +## Current shape after P2.5 + +Juno v1 ships `astroport-incentives` as a bounded LP rewards contract: DAO-funded +internal rewards, permissionless external incentives, no vesting contract, no +xASTRO staking, no maker, and no new DEX token. diff --git a/planning/13-scope-guard-verification-2026-06-28.md b/planning/13-scope-guard-verification-2026-06-28.md new file mode 100644 index 000000000..49b429dc9 --- /dev/null +++ b/planning/13-scope-guard-verification-2026-06-28.md @@ -0,0 +1,42 @@ +# 13 — Juno v1 scope guard verification + +Date: 2026-06-28 + +## Slice + +Verified the local Astroport-Juno v1 scope guard after P2.5 re-added bounded LP incentives to v1. + +## Why it matters + +The launch scope must stay simple: XYK swaps, pools, liquidity, and bounded LP incentives only. The docs now say `contracts/tokenomics/incentives` is included, so `Cargo.toml`, the canonical strip list, and the final wasm artifact set need to agree. + +## Commands run + +```sh +python3 scripts/check_juno_v1_scope.py +git diff --check -- planning/01-strip-list.md planning/04-incentives-types-decision.md scripts/check_juno_v1_scope.py +git status --short --branch +``` + +## Results + +```text +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +``` + +`git diff --check` produced no output, so the changed planning files and new guard script have no whitespace errors. + +## Current local refs + +```text +## main...origin/main + M planning/01-strip-list.md + M planning/04-incentives-types-decision.md +?? scripts/check_juno_v1_scope.py +?? planning/13-scope-guard-verification-2026-06-28.md +``` + +## Next + +Run the broader contract build/check path for the eight expected wasm artifacts, then wire `scripts/check_juno_v1_scope.py` into CI or a documented pre-audit checklist. diff --git a/planning/14-schema-scope-guard-2026-06-28.md b/planning/14-schema-scope-guard-2026-06-28.md new file mode 100644 index 000000000..ba9b5bda8 --- /dev/null +++ b/planning/14-schema-scope-guard-2026-06-28.md @@ -0,0 +1,68 @@ +# 14 — Juno v1 schema scope guard + +Date: 2026-06-28 + +## Slice + +Pruned committed JSON schema directories so frontend/integration consumers only see the eight Astroport-Juno v1 contracts, then added a no-dependency schema-scope guard. + +## Why it matters + +The repository still contained generated schemas for stripped or deferred surfaces (`maker`, `staking`, `vesting`, `xastro_token`, stable/PCL/sale-tax/converter pairs). Those schemas make non-v1 contracts look available even when `Cargo.toml` and the strip plan exclude them. For a Juno-native v1 DEX, the schema surface should advertise only XYK swaps/pools, routing, registry/oracle/tracker/whitelist, and bounded LP incentives. + +## Kept schema directories + +```text +astroport-factory +astroport-incentives +astroport-native-coin-registry +astroport-oracle +astroport-pair +astroport-router +astroport-tokenfactory-tracker +astroport-whitelist +``` + +## Removed stale schema directories + +```text +astro-token-converter +astroport-maker +astroport-pair-concentrated +astroport-pair-concentrated-duality +astroport-pair-concentrated-sale-tax +astroport-pair-converter +astroport-pair-stable +astroport-pair-xastro +astroport-pair-xyk-sale-tax +astroport-staking +astroport-vesting +astroport-xastro-token +``` + +## Verification + +```sh +python3 scripts/check_juno_v1_schemas.py +python3 scripts/check_juno_v1_scope.py +git diff --check -- scripts/check_juno_v1_schemas.py schemas scripts/check_juno_v1_scope.py planning/01-strip-list.md planning/04-incentives-types-decision.md +``` + +Results: + +```text +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +``` + +`git diff --check` produced no output. + +## Build caveat + +This host currently has no `cargo` binary on `PATH`, so I could not regenerate schemas with `scripts/build_schemas.sh` in this run. The new guard is deliberately Python-only and verifies the committed schema surface that frontend work will consume. + +## Next + +Install/activate Rust on this host or run inside the known build container, then execute `scripts/build_schemas.sh` and re-run both Juno v1 guards. diff --git a/planning/15-juno-v1-guard-readiness-2026-06-29.md b/planning/15-juno-v1-guard-readiness-2026-06-29.md new file mode 100644 index 000000000..d847e446b --- /dev/null +++ b/planning/15-juno-v1-guard-readiness-2026-06-29.md @@ -0,0 +1,54 @@ +# 15 — Juno v1 guard readiness check + +Date: 2026-06-29 + +## Slice + +Re-ran the local Astroport-Juno v1 guardrail checks and captured release-readiness state for the simple DEX surface. + +## Why it matters + +Juno DeFi v1 should stay boring and shippable: swaps, pools, liquidity, routing, and bounded LP incentives. The guard scripts keep `Cargo.toml`, planning docs, and committed JSON schemas from quietly drifting back into stable/PCL/converter/staking/vesting scope. + +## Verification run + +```sh +python3 scripts/check_juno_v1_scope.py \ + && python3 scripts/check_juno_v1_schemas.py \ + && git diff --check -- scripts/check_juno_v1_scope.py scripts/check_juno_v1_schemas.py planning/01-strip-list.md planning/04-incentives-types-decision.md planning/13-scope-guard-verification-2026-06-28.md planning/14-schema-scope-guard-2026-06-28.md schemas +command -v cargo || true; command -v rustc || true; command -v just || true +``` + +Results: + +```text +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +``` + +`git diff --check` produced no output. + +Tooling available on this host: + +```text +/usr/bin/just +``` + +No `cargo` or `rustc` binary was found on `PATH`, so full Rust build/schema regeneration remains blocked on a Rust toolchain or build container. + +## Current guarded v1 contract surface + +- `astroport_factory.wasm` +- `astroport_pair.wasm` +- `astroport_router.wasm` +- `astroport_native_coin_registry.wasm` +- `astroport_oracle.wasm` +- `astroport_tokenfactory_tracker.wasm` +- `astroport_whitelist.wasm` +- `astroport_incentives.wasm` + +## Next + +Run the same guards inside the Rust-enabled build environment after regenerating schemas, then wire both Python guards into CI/pre-audit docs before any Juno DEX v1 release branch. diff --git a/planning/16-ci-artifact-guard-2026-06-29.md b/planning/16-ci-artifact-guard-2026-06-29.md new file mode 100644 index 000000000..aec2e3273 --- /dev/null +++ b/planning/16-ci-artifact-guard-2026-06-29.md @@ -0,0 +1,48 @@ +# 16 — CI guard for Juno v1 artifacts + +Date: 2026-06-29 + +## Slice + +Added an optimized-artifact guard and wired the Juno v1 scope/schema/artifact checks into GitHub Actions. + +## Why it matters + +The v1 launch surface should remain a simple Juno-native DEX: factory, XYK pair, router, whitelist, native coin registry, oracle, tokenfactory tracker, and bounded LP incentives. CI now has cheap fail-fast checks before Rust work starts, after schema regeneration, and after rust-optimizer emits wasm artifacts. + +This reduces the launch-risk class where deferred contracts such as stable/PCL pairs, maker, staking, vesting, xASTRO, converters, or sale-tax variants silently re-enter release artifacts. + +## Files touched + +- `scripts/check_juno_v1_artifacts.py` +- `.github/workflows/tests_and_checks.yml` +- `.github/workflows/check_artifacts.yml` + +## CI wiring + +- `tests_and_checks.yml` now runs: + - `python3 scripts/check_juno_v1_scope.py` + - `python3 scripts/check_juno_v1_schemas.py` + - and re-runs the schema guard after `scripts/build_schemas.sh`. +- `check_artifacts.yml` now runs: + - `scripts/check_artifacts_size.sh` + - `python3 scripts/check_juno_v1_artifacts.py` after optimizer output exists. + +## Expected v1 artifact set + +- `astroport_factory.wasm` +- `astroport_pair.wasm` +- `astroport_router.wasm` +- `astroport_native_coin_registry.wasm` +- `astroport_oracle.wasm` +- `astroport_tokenfactory_tracker.wasm` +- `astroport_whitelist.wasm` +- `astroport_incentives.wasm` + +## Verification + +Local host still has no Rust toolchain, and `scripts/build_release.sh` is blocked because the Docker daemon is not reachable here (`Cannot connect to the Docker daemon at unix:///var/run/docker.sock`). The new artifact guard was verified with a temporary fake artifact directory containing exactly the eight expected wasm names, plus a negative extra-artifact check. + +## Next + +Run the CI path or local `scripts/build_release.sh` in a Rust/Docker-enabled environment and confirm `scripts/check_juno_v1_artifacts.py` passes against real optimized wasm output. diff --git a/planning/17-frontend-schema-surface-2026-06-29.md b/planning/17-frontend-schema-surface-2026-06-29.md new file mode 100644 index 000000000..717ae5170 --- /dev/null +++ b/planning/17-frontend-schema-surface-2026-06-29.md @@ -0,0 +1,29 @@ +# Astroport-Juno v1 frontend schema surface + +Generated from committed `schemas/*/raw/*.json`. Keep this surface boring: XYK swap/liquidity, pair discovery, registry/oracle reads, and external incentives only. + +| Contract | Instantiate fields | Execute variants | Query variants | Other messages | Response schemas | +|---|---|---|---|---|---| +| `astroport-factory` | `coin_registry_address`, `fee_address`, `generator_address`, `owner`, `pair_configs`, `token_code_id`, `tracker_config`, `whitelist_code_id` | `update_config`, `update_tracker_config`, `update_pair_config`, `create_pair`, `deregister`, `propose_new_owner`, `drop_ownership_proposal`, `claim_ownership` | `config`, `pair`, `pairs`, `fee_info`, `blacklisted_pair_types`, `tracker_config` | — | `blacklisted_pair_types`, `config`, `fee_info`, `pair`, `pairs`, `tracker_config` | +| `astroport-incentives` | `astro_token`, `factory`, `guardian`, `incentivization_fee_info`, `owner`, `vesting_contract` | `setup_pools`, `claim_rewards`, `receive`, `deposit`, `withdraw`, `set_tokens_per_second`, `incentivize`, `incentivize_many`, `remove_reward_from_pool`, `claim_orphaned_rewards`, `update_config`, `update_blocked_tokenslist`, `deactivate_pool`, `deactivate_blocked_pools`, `propose_new_owner`, `drop_ownership_proposal`, `claim_ownership` | `config`, `deposit`, `pending_rewards`, `reward_info`, `pool_info`, `pool_stakers`, `blocked_tokens_list`, `is_fee_expected`, `external_reward_schedules`, `list_pools`, `active_pools` | — | `active_pools`, `blocked_tokens_list`, `config`, `deposit`, `external_reward_schedules`, `is_fee_expected`, `list_pools`, `pending_rewards`, `pool_info`, `pool_stakers`, `reward_info` | +| `astroport-native-coin-registry` | `owner` | `add`, `register`, `remove`, `propose_new_owner`, `drop_ownership_proposal`, `claim_ownership` | `config`, `native_token`, `native_tokens` | — | `config`, `native_token`, `native_tokens` | +| `astroport-oracle` | `asset_infos`, `factory_contract` | `update` | `consult` | — | `consult` | +| `astroport-pair` | `asset_infos`, `factory_addr`, `init_params`, `pair_type`, `token_code_id` | `receive`, `provide_liquidity`, `withdraw_liquidity`, `swap`, `update_config`, `propose_new_owner`, `drop_ownership_proposal`, `claim_ownership`, `custom` | `pair`, `pool`, `config`, `share`, `simulation`, `reverse_simulation`, `cumulative_prices`, `query_compute_d`, `asset_balance_at`, `observe`, `simulate_withdraw`, `simulate_provide` | — | `asset_balance_at`, `config`, `cumulative_prices`, `observe`, `pair`, `pool`, `query_compute_d`, `reverse_simulation`, `share`, `simulate_provide`, `simulate_withdraw`, `simulation` | +| `astroport-router` | `astroport_factory` | `receive`, `execute_swap_operations`, `execute_swap_operation` | `config`, `simulate_swap_operations`, `reverse_simulate_swap_operations` | — | `config`, `reverse_simulate_swap_operations`, `simulate_swap_operations` | +| `astroport-tokenfactory-tracker` | `tokenfactory_module_address`, `track_over_seconds`, `tracked_denom` | — | `balance_at`, `total_supply_at`, `config` | `sudo:block_before_send`, `sudo:track_before_send` | `balance_at`, `config`, `total_supply_at` | +| `astroport-whitelist` | `admins`, `mutable` | `execute`, `freeze`, `update_admins` | `admin_list`, `can_execute` | — | `admin_list`, `can_execute` | + +contracts=8 + +## Frontend launch implications + +- Use `astroport-factory` for pair discovery and `create_pair`; v1 pair type is XYK only. +- Use `astroport-router` for multi-hop swaps; direct pair `swap` remains the single-hop primitive. +- Liquidity UX must support pair `provide_liquidity`, `withdraw_liquidity`, `simulation`, `reverse_simulation`, `simulate_provide`, and `simulate_withdraw`. +- Incentives UX is external-rewards only for v1: claim/deposit/withdraw plus pool/reward queries. No new DEX token UI, vesting, staking, stable pools, PCL, LSTs, perps, or yield vaults. +- Deployment config that frontends need: factory address, router address, native coin registry address, incentives address, optional oracle addresses, and factory-discovered pair addresses. + +## Verification + +- Generated by `python3 scripts/summarize_juno_v1_schema_surface.py` from committed JSON schemas. +- Guarded by `python3 scripts/check_juno_v1_schemas.py` to keep the eight-contract v1 surface exact. diff --git a/planning/18-deployment-template-guard-2026-06-29.md b/planning/18-deployment-template-guard-2026-06-29.md new file mode 100644 index 000000000..f36226c85 --- /dev/null +++ b/planning/18-deployment-template-guard-2026-06-29.md @@ -0,0 +1,47 @@ +# 18 — Juno v1 deployment template guard + +Date: 2026-06-29 + +## Increment + +Added a minimal testnet deployment config template and a no-dependency validator for the Astroport-Juno v1 launch surface. + +The template is intentionally boring: code IDs, contract addresses, instantiate messages, and frontend-required addresses for factory, router, native coin registry, incentives, oracle, tokenfactory tracker, and whitelist. It does not add a DEX token, stable pools, PCL, LSTs, perps, yield vaults, or other deferred surfaces. + +## Files + +- `deployment/juno-v1-testnet.template.json` +- `scripts/check_juno_v1_deployment_template.py` + +## Guard behavior + +`check_juno_v1_deployment_template.py` reads committed `schemas/*/raw/instantiate.json` and verifies: + +- the template has instantiate messages for each directly instantiated v1 contract; +- each instantiate message includes all schema-required fields; +- code IDs include exactly the v1 contracts plus `cw20-base` for LP tokens; +- addresses include exactly the directly instantiated v1 contracts; +- factory `pair_configs` contains exactly one permissionless XYK config; +- the pair creation template is XYK-only; +- frontend-required addresses resolve to template addresses. + +## Verification + +```text +$ python3 scripts/check_juno_v1_deployment_template.py +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk + +$ python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 + +$ git diff --check -- deployment/juno-v1-testnet.template.json scripts/check_juno_v1_deployment_template.py planning/18-deployment-template-guard-2026-06-29.md planning/00-overview.md +# no output +``` + +## Next bounded slice + +Wire the deployment template guard into CI, then replace placeholders with real uni-7 code IDs/addresses after a successful optimized artifact build and upload. diff --git a/planning/19-ci-deployment-template-guard-2026-06-29.md b/planning/19-ci-deployment-template-guard-2026-06-29.md new file mode 100644 index 000000000..f1212b4f4 --- /dev/null +++ b/planning/19-ci-deployment-template-guard-2026-06-29.md @@ -0,0 +1,39 @@ +# 19 — CI deployment template guard + +Date: 2026-06-29 + +## Increment + +Wired the Juno v1 deployment template validator into the normal GitHub Actions test/check path. + +The first CI step now runs all no-dependency launch guards before Rust install/cache-heavy work: + +1. `scripts/check_juno_v1_scope.py` +2. `scripts/check_juno_v1_schemas.py` +3. `scripts/check_juno_v1_deployment_template.py` + +This keeps the v1 launch surface boring and exact: XYK pools only, no stable/PCL/perps/yield creep, and a deployment template that tracks the instantiate schemas. + +## Files + +- `.github/workflows/tests_and_checks.yml` +- `planning/19-ci-deployment-template-guard-2026-06-29.md` + +## Verification + +```text +$ python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py && python3 scripts/check_juno_v1_deployment_template.py +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk + +$ git diff --check -- .github/workflows/tests_and_checks.yml planning/19-ci-deployment-template-guard-2026-06-29.md +# no output +``` + +## Next bounded slice + +Run the GitHub Actions path on a branch/PR, or locally run the Rust checks after schema generation to make sure the full workflow still passes end-to-end. diff --git a/planning/20-ci-wiring-guard-2026-06-29.md b/planning/20-ci-wiring-guard-2026-06-29.md new file mode 100644 index 000000000..b9d530b69 --- /dev/null +++ b/planning/20-ci-wiring-guard-2026-06-29.md @@ -0,0 +1,42 @@ +# 20 — CI wiring guard + +Date: 2026-06-29 + +## Increment + +Added a dependency-free GitHub Actions wiring validator for the Juno v1 launch guards. + +The new `scripts/check_juno_v1_ci_wiring.py` scans workflow text and fails if: + +- the scope/schema/deployment-template guards stop running before Rust install/cache-heavy work; +- the schema guard stops running again after schema regeneration and before the schema diff check; +- the artifact-set guard stops running after optimizer output and artifact size checks but before `cosmwasm-check`. + +This is deliberately boring infrastructure: if someone edits CI while rushing toward uni-7, the launch guards should not silently become documentation theater. + +## Files + +- `.github/workflows/tests_and_checks.yml` +- `scripts/check_juno_v1_ci_wiring.py` +- `planning/20-ci-wiring-guard-2026-06-29.md` + +## Verification + +```text +$ python3 scripts/check_juno_v1_ci_wiring.py && python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py && python3 scripts/check_juno_v1_deployment_template.py +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template pre_rust=true schema_post_generation=true artifact_guard_after_size=true +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk + +$ git diff --check -- .github/workflows/tests_and_checks.yml scripts/check_juno_v1_ci_wiring.py planning/20-ci-wiring-guard-2026-06-29.md +# no output +``` + +## Next bounded slice + +Add a minimal deployment fill script that takes real uni-7 code IDs/addresses and updates a copied config while preserving the template guard’s v1 constraints. diff --git a/planning/21-deployment-fill-script-2026-06-29.md b/planning/21-deployment-fill-script-2026-06-29.md new file mode 100644 index 000000000..32a358919 --- /dev/null +++ b/planning/21-deployment-fill-script-2026-06-29.md @@ -0,0 +1,42 @@ +# 21 — Deployment fill script + +Date: 2026-06-29 + +## Increment + +Added a small, dependency-free renderer for the uni-7 deployment/frontend config handoff: + +- `scripts/fill_juno_v1_deployment_config.py` + +It starts from `deployment/juno-v1-testnet.template.json`, accepts repeated `--set dotted.path=value` overrides for real code IDs, addresses, accounts, and network values, then rewires dependent instantiate fields from those top-level values. + +This keeps the v1 deployment surface boring: + +- XYK-only pair config remains in the template/guard. +- No DEX token is introduced; incentives still use the configured native denom. +- Frontend-required contract addresses stay in one top-level `addresses` section. +- `--require-complete` fails if placeholder strings remain or any code ID is still `0`. + +## Guard update + +`check_juno_v1_deployment_template.py` now accepts an optional config path, so both the placeholder template and a rendered concrete config can be checked with the same schema-derived guard. + +## Verification + +Ran a temp render with dummy uni-7 values and `--require-complete`, then validated the rendered config: + +```sh +python3 scripts/fill_juno_v1_deployment_config.py --output /tmp/juno-v1-filled.json --require-complete ... +# OK: wrote rendered Juno v1 deployment config to /tmp/juno-v1-filled.json +# sets=21 require_complete=True + +python3 scripts/check_juno_v1_deployment_template.py /tmp/juno-v1-filled.json +# OK: Juno v1 deployment template matches instantiate schema requirements +# instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +``` + +Also re-ran the default template guard to ensure existing CI behavior is unchanged. + +## Next bounded slice + +Add a sample `deployment/README.md` command block showing the exact fill command shape for uni-7 once upload/instantiate outputs are known. diff --git a/planning/22-deployment-readme-2026-06-29.md b/planning/22-deployment-readme-2026-06-29.md new file mode 100644 index 000000000..3d852f093 --- /dev/null +++ b/planning/22-deployment-readme-2026-06-29.md @@ -0,0 +1,51 @@ +# 22 — Deployment handoff README + +Date: 2026-06-29 + +## Increment + +Added `deployment/README.md` as the operator/frontend handoff for the uni-7 bakeoff. + +The README lists the exact values that must be collected from real upload and instantiate output before rendering a concrete config: + +- four governance/operator accounts; +- nine code IDs; +- seven instantiated contract addresses; +- one real first-pool counterpart denom. + +It also includes a copy/paste `fill_juno_v1_deployment_config.py --require-complete` command and the follow-up template guard command so the rendered `deployment/juno-v1-testnet.json` can be checked before frontend use. + +## Scope guardrails + +The README keeps the v1 surface narrow: + +- XYK-only and permissionless; +- no new DEX token; +- no stable pairs, LSTs, perps, or yield surfaces; +- frontend discovers pools through the factory instead of hardcoding prelaunch pools. + +## Verification + +```text +$ python3 scripts/fill_juno_v1_deployment_config.py --output /tmp/juno-v1-readme-check.json --require-complete ... +OK: wrote rendered Juno v1 deployment config to /tmp/juno-v1-readme-check.json +sets=21 require_complete=True + +$ python3 scripts/check_juno_v1_deployment_template.py /tmp/juno-v1-readme-check.json +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk + +$ python3 scripts/check_juno_v1_deployment_template.py && python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py && python3 scripts/check_juno_v1_ci_wiring.py +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template pre_rust=true schema_post_generation=true artifact_guard_after_size=true +``` + +## Next bounded slice + +Add a small upload-output parser/checklist that can turn `junod tx wasm store` / `instantiate` JSON logs into the `--set` values for this README command. diff --git a/planning/23-tx-json-extraction-helper-2026-06-29.md b/planning/23-tx-json-extraction-helper-2026-06-29.md new file mode 100644 index 000000000..e72898222 --- /dev/null +++ b/planning/23-tx-json-extraction-helper-2026-06-29.md @@ -0,0 +1,37 @@ +# 23 — Tx JSON extraction helper + +Date: 2026-06-29 + +## Increment + +Added `scripts/extract_juno_v1_tx_sets.py`, a small operator helper for the Astroport-Juno v1 uni-7 deployment handoff. + +It parses common `junod -o json` tx response shapes and extracts: + +- wasm upload `code_id` attributes into `--set code_ids.=...` flags; +- instantiate `_contract_address` / `contract_address` attributes into `--set addresses.=...` flags; +- unmapped `--scan` output for unfamiliar tx JSON before assigning names. + +Also linked the helper from `deployment/README.md` so operators have the path from tx logs to `fill_juno_v1_deployment_config.py`. + +## Verification + +```text +$ python3 scripts/extract_juno_v1_tx_sets.py --code-id astroport-factory=target/juno-v1-store-sample.json --address astroport-factory=target/juno-v1-instantiate-sample.json +--set code_ids.astroport-factory='77' +--set addresses.astroport-factory='juno1factory000000000000000000000000000000000' + +$ python3 scripts/extract_juno_v1_tx_sets.py --scan target/juno-v1-store-sample.json target/juno-v1-instantiate-sample.json +# target/juno-v1-store-sample.json +code_ids=77 +addresses=- +# target/juno-v1-instantiate-sample.json +code_ids=- +addresses=juno1factory000000000000000000000000000000000 + +$ python3 scripts/extract_juno_v1_tx_sets.py --code-id not-a-contract=target/juno-v1-store-sample.json >/tmp/juno-bad-key.out 2>/tmp/juno-bad-key.err; test $? -ne 0 +``` + +## Next bounded slice + +Add a tiny fixture-based test command for `extract_juno_v1_tx_sets.py` to CI, or use it against the first real uni-7 upload tx outputs when they exist. diff --git a/planning/24-tx-extractor-fixture-guard-2026-06-29.md b/planning/24-tx-extractor-fixture-guard-2026-06-29.md new file mode 100644 index 000000000..03ae11634 --- /dev/null +++ b/planning/24-tx-extractor-fixture-guard-2026-06-29.md @@ -0,0 +1,38 @@ +# 24 — Tx extractor fixture guard + +Date: 2026-06-29 + +## Increment + +Added `scripts/check_juno_v1_tx_extractor.py`, a dependency-free fixture guard for the deployment tx parser. + +The guard exercises the operator handoff path before any live uni-7 outputs exist: + +- maps a `tx_response.events` wasm upload `code_id` into `--set code_ids.astroport-factory=...`; +- maps instantiate addresses from both `logs[].events` and JSON-encoded `raw_log` shapes; +- verifies unmapped `--scan` output surfaces discovered code IDs/addresses; +- verifies failure behavior for unknown contract keys and ambiguous multi-code tx files. + +Wired this guard into `.github/workflows/tests_and_checks.yml` before Rust setup, and extended `scripts/check_juno_v1_ci_wiring.py` so CI fails if the tx extractor guard is removed or moved after expensive Rust work. + +## Verification + +```text +$ python3 scripts/check_juno_v1_tx_extractor.py && python3 scripts/check_juno_v1_ci_wiring.py && python3 scripts/check_juno_v1_deployment_template.py && python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py && git diff --check -- .github/workflows/tests_and_checks.yml scripts/check_juno_v1_ci_wiring.py scripts/check_juno_v1_tx_extractor.py +OK: Juno v1 tx extractor handles mapped, scan, raw_log, and failure cases +fixtures=4 mapped_sets=3 failure_cases=2 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor pre_rust=true schema_post_generation=true artifact_guard_after_size=true +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Add a dry-run deployment value bundle/checklist that combines extractor output plus accounts/counterparty denom into one render command, or run the extractor against the first real uni-7 upload/instantiate tx JSON when available. diff --git a/planning/25-deployment-command-bundle-2026-06-29.md b/planning/25-deployment-command-bundle-2026-06-29.md new file mode 100644 index 000000000..e8c7210eb --- /dev/null +++ b/planning/25-deployment-command-bundle-2026-06-29.md @@ -0,0 +1,38 @@ +# 25 — Deployment command bundle + +Date: 2026-06-29 + +## Increment + +Added `scripts/build_juno_v1_deployment_command.py`, a dependency-free handoff helper that combines: + +- tx-derived `--set` lines from `scripts/extract_juno_v1_tx_sets.py` for the 9 v1 code IDs and 7 instantiated contract addresses; and +- manual operator values for `owner`, `guardian`, `treasury`, tokenfactory module address, and the first counterparty denom in the sample XYK pair-create template. + +The helper prints one copy/paste-safe `scripts/fill_juno_v1_deployment_config.py --require-complete` command. With `--render`, it executes that command and immediately validates the rendered config through `scripts/check_juno_v1_deployment_template.py`. + +Added `scripts/check_juno_v1_deployment_command.py` to fixture-test the happy path, render+guard path, and missing tx-value failure. Wired it into `.github/workflows/tests_and_checks.yml` before Rust setup, and extended `scripts/check_juno_v1_ci_wiring.py` so CI fails if this handoff guard is removed or reordered after expensive Rust work. + +## Verification + +```text +$ python3 scripts/check_juno_v1_deployment_command.py && python3 scripts/check_juno_v1_ci_wiring.py && python3 scripts/check_juno_v1_tx_extractor.py && python3 scripts/check_juno_v1_deployment_template.py && python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py && git diff --check -- .github/workflows/tests_and_checks.yml scripts/check_juno_v1_ci_wiring.py scripts/build_juno_v1_deployment_command.py scripts/check_juno_v1_deployment_command.py +OK: Juno v1 deployment command builder combines tx sets and manual values +sets=21 tx_sets=16 manual_sets=5 render_guard=true failure_cases=1 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command pre_rust=true schema_post_generation=true artifact_guard_after_size=true +OK: Juno v1 tx extractor handles mapped, scan, raw_log, and failure cases +fixtures=4 mapped_sets=3 failure_cases=2 +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Use the bundle helper against the first real uni-7 upload/instantiate tx JSON, or add a tiny operator checklist that names the expected tx JSON filenames for all 16 tx-derived values. diff --git a/planning/26-operator-tx-checklist-guard-2026-06-29.md b/planning/26-operator-tx-checklist-guard-2026-06-29.md new file mode 100644 index 000000000..4fba2dcb9 --- /dev/null +++ b/planning/26-operator-tx-checklist-guard-2026-06-29.md @@ -0,0 +1,40 @@ +# 26 — Operator tx checklist guard + +Date: 2026-06-29 + +## Increment + +Added a guarded operator checklist for the uni-7 Astroport-Juno v1 deployment handoff. The checklist names the exact 16 transaction JSON files expected after `junod -o json` upload/instantiate work: + +- 9 store tx files for v1 code IDs: factory, incentives, native coin registry, oracle, pair, router, tokenfactory tracker, whitelist, and `cw20-base`. +- 7 instantiate tx files for v1 contract addresses: factory, incentives, native coin registry, oracle, router, tokenfactory tracker, and whitelist. + +Added `scripts/check_juno_v1_operator_checklist.py`, a dependency-free guard that verifies the checklist stays aligned with `scripts/extract_juno_v1_tx_sets.py`, `scripts/build_juno_v1_deployment_command.py`, the deployment README link, the required 5 manual operator values, and the narrow v1 scope guardrail. + +Wired the checklist guard into `.github/workflows/tests_and_checks.yml` before Rust setup and extended `scripts/check_juno_v1_ci_wiring.py` so CI fails if the guard is removed or reordered after expensive Rust work. Also ignored local deployment tx JSON and rendered `deployment/juno-v1-testnet.json` to reduce the chance that real tx output or environment-specific config is committed accidentally. + +## Verification + +```text +$ python3 scripts/check_juno_v1_operator_checklist.py && python3 scripts/check_juno_v1_ci_wiring.py && python3 scripts/check_juno_v1_deployment_command.py && python3 scripts/check_juno_v1_tx_extractor.py && python3 scripts/check_juno_v1_deployment_template.py && python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py +OK: Juno v1 operator tx checklist matches deployment helpers +store_txs=9 instantiate_txs=7 manual_values=5 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true schema_post_generation=true artifact_guard_after_size=true +OK: Juno v1 deployment command builder combines tx sets and manual values +sets=21 tx_sets=16 manual_sets=5 render_guard=true failure_cases=1 +OK: Juno v1 tx extractor handles mapped, scan, raw_log, and failure cases +fixtures=4 mapped_sets=3 failure_cases=2 +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +``` + +`git diff --check` produced no output for the touched files. + +## Next bounded slice + +Add a tiny dry-run tx fixture generator for the 16 expected uni-7 tx JSON files so operators can rehearse the full extractor → bundle → render flow without real chain txs, then replace fixtures with actual uni-7 outputs when available. diff --git a/planning/27-dry-run-tx-fixtures-2026-06-29.md b/planning/27-dry-run-tx-fixtures-2026-06-29.md new file mode 100644 index 000000000..2186fce3a --- /dev/null +++ b/planning/27-dry-run-tx-fixtures-2026-06-29.md @@ -0,0 +1,36 @@ +# 27 — Dry-run tx fixture rehearsal + +Date: 2026-06-29 + +## Increment + +Added a rehearsal path for the Astroport-Juno v1 uni-7 deployment handoff: + +- `scripts/generate_juno_v1_dry_run_txs.py` writes the exact 16 tx JSON filenames expected by the operator checklist, using harmless synthetic `code_id` and `_contract_address` events. +- `scripts/check_juno_v1_dry_run_txs.py` proves the full local path works: generator → extractor → `tx-sets.txt` → deployment command builder with `--render` → template guard. +- `deployment/README.md` now points operators at the dry-run rehearsal before real chain tx output exists. + +This keeps DeFi v1 boring and narrow: swaps/pools/liquidity deployment plumbing only, no scope creep. + +## Verification + +```text +$ python3 scripts/check_juno_v1_dry_run_txs.py && python3 scripts/check_juno_v1_operator_checklist.py && python3 scripts/check_juno_v1_deployment_command.py && python3 scripts/check_juno_v1_tx_extractor.py && python3 scripts/check_juno_v1_deployment_template.py && git diff --check -- scripts/generate_juno_v1_dry_run_txs.py scripts/check_juno_v1_dry_run_txs.py deployment/README.md +OK: Juno v1 dry-run tx fixtures exercise generator -> extractor -> builder -> template guard +fixture_files=16 tx_sets=16 render_guard=true +store_txs=9 instantiate_txs=7 total=16 +OK: Juno v1 operator tx checklist matches deployment helpers +store_txs=9 instantiate_txs=7 manual_values=5 +OK: Juno v1 deployment command builder combines tx sets and manual values +sets=21 tx_sets=16 manual_sets=5 render_guard=true failure_cases=1 +OK: Juno v1 tx extractor handles mapped, scan, raw_log, and failure cases +fixtures=4 mapped_sets=3 failure_cases=2 +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Wire `scripts/check_juno_v1_dry_run_txs.py` into the cheap pre-Rust CI guard sequence, then extend `scripts/check_juno_v1_ci_wiring.py` so it fails if the rehearsal guard disappears. diff --git a/planning/28-dry-run-ci-wiring-2026-06-29.md b/planning/28-dry-run-ci-wiring-2026-06-29.md new file mode 100644 index 000000000..91b68a873 --- /dev/null +++ b/planning/28-dry-run-ci-wiring-2026-06-29.md @@ -0,0 +1,31 @@ +# 28 — Dry-run tx rehearsal CI wiring + +Date: 2026-06-29T07:15:42Z + +## Increment + +Wired the Astroport-Juno v1 dry-run deployment rehearsal into the cheap pre-Rust GitHub Actions guard sequence. + +Changed: + +- `.github/workflows/tests_and_checks.yml` now runs `scripts/check_juno_v1_dry_run_txs.py` before the self-checking CI wiring guard. +- `scripts/check_juno_v1_ci_wiring.py` now fails if the dry-run rehearsal guard disappears or moves after Rust setup. + +This protects the uni-7 handoff path: synthetic tx fixtures → extractor → tx set file → deployment command builder → rendered template guard. + +## Verification + +```text +$ python3 scripts/check_juno_v1_dry_run_txs.py && python3 scripts/check_juno_v1_ci_wiring.py && git diff --check -- .github/workflows/tests_and_checks.yml scripts/check_juno_v1_ci_wiring.py +OK: Juno v1 dry-run tx fixtures exercise generator -> extractor -> builder -> template guard +fixture_files=16 tx_sets=16 render_guard=true +store_txs=9 instantiate_txs=7 total=16 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true dry_run_txs=true schema_post_generation=true artifact_guard_after_size=true +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Make the final operator path even harder to misuse: add a tiny guard that confirms `deployment/tx/uni-7/` stays gitignored and that generated dry-run tx JSON never gets committed. diff --git a/planning/29-deployment-gitignore-guard-2026-06-29.md b/planning/29-deployment-gitignore-guard-2026-06-29.md new file mode 100644 index 000000000..0153c2071 --- /dev/null +++ b/planning/29-deployment-gitignore-guard-2026-06-29.md @@ -0,0 +1,45 @@ +# 29 — Deployment artifact gitignore guard + +Date: 2026-06-29T07:44:26Z + +## Increment + +Added a cheap safety guard for the Astroport-Juno v1 uni-7 handoff local artifacts: + +- `scripts/check_juno_v1_deployment_gitignore.py` verifies `.gitignore` keeps `deployment/tx/` and `deployment/juno-v1-testnet.json` ignored. +- The guard uses `git check-ignore --no-index` against representative real and dry-run tx paths, confirms no `deployment/tx/` or rendered `deployment/juno-v1-testnet.json` artifacts are tracked, and ensures the dry-run generator default stays under ignored `deployment/tx/`. +- `.github/workflows/tests_and_checks.yml` now runs the guard in the pre-Rust launch-guard sequence. +- `scripts/check_juno_v1_ci_wiring.py` now fails if the deployment gitignore guard disappears or moves after Rust setup. + +This reduces launch risk by keeping operator tx JSON and rendered configs local until stewards intentionally publish final uni-7 values. + +## Verification + +```text +$ python3 scripts/check_juno_v1_deployment_gitignore.py && python3 scripts/check_juno_v1_ci_wiring.py && python3 scripts/check_juno_v1_dry_run_txs.py && python3 scripts/check_juno_v1_operator_checklist.py && python3 scripts/check_juno_v1_deployment_command.py && python3 scripts/check_juno_v1_tx_extractor.py && python3 scripts/check_juno_v1_deployment_template.py && python3 scripts/check_juno_v1_scope.py && python3 scripts/check_juno_v1_schemas.py && git diff --check -- scripts/check_juno_v1_deployment_gitignore.py scripts/check_juno_v1_ci_wiring.py .github/workflows/tests_and_checks.yml +OK: Juno v1 deployment tx/output paths stay gitignored +ignored_paths=5 tracked_artifacts=0 generator_default=deployment/tx/uni-7-dry-run +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true dry_run_txs=true deployment_gitignore=true schema_post_generation=true artifact_guard_after_size=true +OK: Juno v1 dry-run tx fixtures exercise generator -> extractor -> builder -> template guard +fixture_files=16 tx_sets=16 render_guard=true +store_txs=9 instantiate_txs=7 total=16 +OK: Juno v1 operator tx checklist matches deployment helpers +store_txs=9 instantiate_txs=7 manual_values=5 +OK: Juno v1 deployment command builder combines tx sets and manual values +sets=21 tx_sets=16 manual_sets=5 render_guard=true failure_cases=1 +OK: Juno v1 tx extractor handles mapped, scan, raw_log, and failure cases +fixtures=4 mapped_sets=3 failure_cases=2 +OK: Juno v1 deployment template matches instantiate schema requirements +instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk +OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md +workspace_members=13 expected_wasms=8 +OK: committed schemas match Astroport-Juno v1 contract set +schema_dirs=8 expected=8 +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Add a small frontend config sanity guard that reads a rendered deployment config and verifies the frontend-facing `addresses` + first XYK pair template are internally consistent without needing chain access. diff --git a/planning/30-frontend-config-guard-2026-06-29.md b/planning/30-frontend-config-guard-2026-06-29.md new file mode 100644 index 000000000..9d87ea31b --- /dev/null +++ b/planning/30-frontend-config-guard-2026-06-29.md @@ -0,0 +1,32 @@ +# 30 — Frontend config handoff guard + +Date: 2026-06-29T08:25:39Z + +## Increment + +Added an offline, dependency-free frontend sanity guard for the Astroport-Juno v1 uni-7 deployment handoff: + +- `scripts/check_juno_v1_frontend_config.py` validates the config consumed by a DEX frontend. +- It confirms `frontend.required_addresses` and `frontend.optional_addresses` stay on the intended small v1 surface. +- It checks frontend address keys exist in top-level `addresses` and that factory/router/incentives/oracle instantiate messages point back to those canonical addresses. +- It rejects pre-launch hardcoded `frontend.pools` / `frontend.pairs` and requires factory-based pair discovery. +- It verifies the first pool template remains simple: XYK-only, exactly two native assets, first asset equals `network.native_asset_denom`, counterparty differs, and `init_params` is `null`. +- CI now runs this guard before the CI-wiring guard, still before Rust setup. + +This gives the future frontend handoff a cheap red light if a rendered deployment config drifts from the simple v1 DEX scope: swaps, pools, liquidity; no hardcoded pools or expanded product surface. + +## Verification + +```text +$ python3 scripts/check_juno_v1_frontend_config.py && python3 scripts/check_juno_v1_ci_wiring.py && git diff --check -- scripts/check_juno_v1_frontend_config.py scripts/check_juno_v1_ci_wiring.py .github/workflows/tests_and_checks.yml planning/00-overview.md planning/30-frontend-config-guard-2026-06-29.md +OK: Juno v1 frontend config handoff is internally consistent +required_addresses=4 optional_addresses=1 native=ujunox pair_type=xyk factory_ref=juno1replacefactory000000000000000000000000000000 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true dry_run_txs=true deployment_gitignore=true frontend_config=true schema_post_generation=true artifact_guard_after_size=true +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Run the dry-run renderer into an ignored temp config and validate that concrete rendered output with both `check_juno_v1_deployment_template.py` and `check_juno_v1_frontend_config.py`. diff --git a/planning/31-dry-run-frontend-validation-2026-06-29.md b/planning/31-dry-run-frontend-validation-2026-06-29.md new file mode 100644 index 000000000..dcba64edc --- /dev/null +++ b/planning/31-dry-run-frontend-validation-2026-06-29.md @@ -0,0 +1,32 @@ +# 31 — Dry-run rendered frontend validation + +Date: 2026-06-29T08:31:45Z + +## Increment + +Extended the existing dry-run deployment rehearsal so it now validates the frontend handoff against a concrete rendered config, not only the placeholder template: + +- `scripts/check_juno_v1_dry_run_txs.py` still generates 16 synthetic uni-7 tx JSON files, extracts the 16 tx-derived `--set` values, and renders a complete deployment config through `scripts/build_juno_v1_deployment_command.py --render`. +- It now also runs `scripts/check_juno_v1_frontend_config.py ` against that temp rendered output. +- This catches a real operator-handoff class of drift: tx-derived addresses/code IDs plus manual values can pass instantiate-schema checks but still break frontend address wiring or the simple first XYK pair template. + +This is still offline and dependency-free. It does not assert real chain deployment success; it proves the rehearsal path can produce a config that both deployment and frontend guards accept. + +## Verification + +```text +$ python3 scripts/check_juno_v1_dry_run_txs.py && python3 scripts/check_juno_v1_frontend_config.py && python3 scripts/check_juno_v1_ci_wiring.py && git diff --check -- scripts/check_juno_v1_dry_run_txs.py +OK: Juno v1 dry-run tx fixtures exercise generator -> extractor -> builder -> template guard -> frontend guard +fixture_files=16 tx_sets=16 render_guard=true frontend_guard=true +store_txs=9 instantiate_txs=7 total=16 +OK: Juno v1 frontend config handoff is internally consistent +required_addresses=4 optional_addresses=1 native=ujunox pair_type=xyk factory_ref=juno1replacefactory000000000000000000000000000000 +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true dry_run_txs=true deployment_gitignore=true frontend_config=true schema_post_generation=true artifact_guard_after_size=true +``` + +`git diff --check` produced no output. + +## Next bounded slice + +Add a small frontend handoff JSON schema/TypeScript type generator from `deployment/juno-v1-testnet.template.json`, or run this rehearsal against real uni-7 tx JSON once the first upload/instantiate outputs exist. diff --git a/planning/32-frontend-types-handoff-2026-06-29.md b/planning/32-frontend-types-handoff-2026-06-29.md new file mode 100644 index 000000000..3a52184e3 --- /dev/null +++ b/planning/32-frontend-types-handoff-2026-06-29.md @@ -0,0 +1,49 @@ +# 32 — Frontend TypeScript handoff guard + +Date: 2026-06-29 + +## Increment + +Generated a narrow TypeScript declaration file for the Astroport-Juno v1 frontend deployment handoff from the canonical deployment template. + +## Why + +Frontend integration should not infer launch scope from ad hoc JSON or stale Astroport surfaces. The handoff type exposes the exact v1 keys a UI needs after uni-7 render: + +- network metadata +- v1 code ID keys +- v1 deployed address keys +- required frontend addresses: factory, router, native coin registry, incentives +- optional frontend address: oracle +- XYK-only first-pair create template + +It deliberately excludes prelaunch hardcoded pools/pairs, stable/PCL variants, DEX-token surfaces, and other post-v1 scope. + +## Files + +- `scripts/generate_juno_v1_frontend_types.py` +- `deployment/juno-v1-frontend-config.d.ts` +- `.github/workflows/tests_and_checks.yml` +- `scripts/check_juno_v1_ci_wiring.py` + +## Verification + +```sh +python3 scripts/generate_juno_v1_frontend_types.py --check +python3 scripts/check_juno_v1_frontend_config.py +python3 scripts/check_juno_v1_ci_wiring.py +python3 scripts/check_juno_v1_dry_run_txs.py +python3 scripts/check_juno_v1_deployment_template.py +git diff --check -- deployment/juno-v1-frontend-config.d.ts scripts/generate_juno_v1_frontend_types.py .github/workflows/tests_and_checks.yml scripts/check_juno_v1_ci_wiring.py planning/00-overview.md planning/32-frontend-types-handoff-2026-06-29.md +``` + +Expected results from this run: + +- TypeScript generator check passes and reports the generated declaration path. +- Frontend config guard passes with required/optional addresses and XYK factory discovery intact. +- CI wiring guard passes and enforces the type check before Rust work. +- Dry-run tx rehearsal still renders a config accepted by deployment and frontend guards. + +## Next bounded slice + +Add a tiny frontend example fixture that imports/copies `JunoV1FrontendDeploymentConfig` and validates a rendered config shape locally, or run the deployment bundle against real uni-7 tx JSON once available. diff --git a/planning/33-frontend-example-guard-2026-06-29.md b/planning/33-frontend-example-guard-2026-06-29.md new file mode 100644 index 000000000..165a0f73c --- /dev/null +++ b/planning/33-frontend-example-guard-2026-06-29.md @@ -0,0 +1,38 @@ +# 33 — Frontend example handoff guard + +## Increment + +Added a tiny TypeScript consumer fixture for the Astroport-Juno v1 frontend deployment handoff: + +- `deployment/juno-v1-frontend-config.example.ts` imports the generated `JunoV1FrontendDeploymentConfig` type. +- The example uses `satisfies JunoV1FrontendDeploymentConfig` against the current uni-7 placeholder shape. +- It exposes a minimal frontend address map helper and a first XYK pair-create template helper. +- It explicitly keeps pair discovery at the factory and does not hardcode launch pools/pairs. + +Added `scripts/check_juno_v1_frontend_example.py`, a dependency-free guard that verifies the example stays aligned with: + +- `deployment/juno-v1-testnet.template.json` code ID keys +- deployment address keys +- frontend required/optional address arrays +- generated `JunoV1AddressKey` union +- v1 XYK-only/no-token/no-stable/no-PCL scope + +Wired the new guard into `.github/workflows/tests_and_checks.yml` and extended the CI wiring guard so the example check must run before Rust setup and after frontend type generation. + +## Verification + +```console +$ python3 scripts/check_juno_v1_frontend_example.py +OK: Juno v1 frontend TypeScript example consumes the generated handoff type +code_ids=9 addresses=7 required=4 optional=1 pair_type=xyk + +$ python3 scripts/check_juno_v1_ci_wiring.py +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true dry_run_txs=true deployment_gitignore=true frontend_config=true frontend_types=true frontend_example=true schema_post_generation=true artifact_guard_after_size=true +``` + +Also reran the frontend type/config guards plus scope/schema/deployment/dry-run guards in this slice. + +## Next bounded slice + +Run the dry-run deployment bundle and frontend example guard against the first real uni-7 tx JSON outputs when upload/instantiate transactions exist, or add a short frontend README snippet showing how a frontend repo should import the rendered JSON plus this declaration file. diff --git a/planning/34-frontend-readme-consumption-2026-06-29.md b/planning/34-frontend-readme-consumption-2026-06-29.md new file mode 100644 index 000000000..fe83df8ee --- /dev/null +++ b/planning/34-frontend-readme-consumption-2026-06-29.md @@ -0,0 +1,32 @@ +# 34 — Frontend README consumption snippet + +## Increment + +Added a focused frontend-consumption section to `deployment/README.md` for the Astroport-Juno v1 handoff. + +The snippet shows frontend builders how to: + +- import the rendered `juno-v1-testnet.json`, +- bind it to `JunoV1FrontendDeploymentConfig`, +- read canonical contract addresses from `config.addresses`, +- use the first XYK pair-create template only as a launch form seed, +- keep existing pool discovery routed through factory queries instead of hardcoded pool addresses. + +## Verification + +```console +$ python3 scripts/check_juno_v1_frontend_example.py +OK: Juno v1 frontend TypeScript example consumes the generated handoff type +code_ids=9 addresses=7 required=4 optional=1 pair_type=xyk + +$ python3 scripts/check_juno_v1_frontend_config.py +OK: Juno v1 frontend config handoff is internally consistent +required_addresses=4 optional_addresses=1 native=ujunox pair_type=xyk factory_ref=juno1replacefactory000000000000000000000000000000 + +$ git diff --check -- deployment/README.md planning/34-frontend-readme-consumption-2026-06-29.md +# no output +``` + +## Next bounded slice + +When real uni-7 upload/instantiate transaction JSON exists, run the deployment command bundle and confirm the rendered `juno-v1-testnet.json` still satisfies the frontend config/example guards. diff --git a/planning/35-deployment-readme-guard-2026-06-29.md b/planning/35-deployment-readme-guard-2026-06-29.md new file mode 100644 index 000000000..5228901aa --- /dev/null +++ b/planning/35-deployment-readme-guard-2026-06-29.md @@ -0,0 +1,35 @@ +# 35 — Deployment README handoff guard + +## Increment + +Added `scripts/check_juno_v1_deployment_readme.py`, a dependency-free guard for the uni-7 deployment handoff README. + +The guard keeps `deployment/README.md` aligned with the launch helpers by checking: + +- required operator/frontend sections, +- dry-run and extraction commands, +- the full render command shape with 4 account, 9 code ID, 7 address, and 1 first-pool denom `--set` values, +- generated frontend handoff files are present, +- the TypeScript `satisfies JunoV1FrontendDeploymentConfig` consumption snippet remains documented, +- v1 scope guardrails stay explicit: XYK-only, permissionless, no DEX token, no stable/LST/perps/yield scope, and factory-based pool discovery. + +Wired the guard into `.github/workflows/tests_and_checks.yml` before Rust setup and extended `scripts/check_juno_v1_ci_wiring.py` so CI fails if the README guard disappears or runs out of order. + +## Verification + +```console +$ python3 scripts/check_juno_v1_deployment_readme.py +OK: Juno v1 deployment README matches operator/frontend handoff helpers +account_sets=4 code_id_sets=9 address_sets=7 frontend_snippet=true scope_guardrails=true + +$ python3 scripts/check_juno_v1_ci_wiring.py +OK: GitHub Actions wiring enforces Astroport-Juno v1 guards +tests_guards=scope/schema/template/tx-extractor/deployment-command/operator-checklist pre_rust=true dry_run_txs=true deployment_gitignore=true deployment_readme=true frontend_config=true frontend_types=true frontend_example=true schema_post_generation=true artifact_guard_after_size=true + +$ git diff --check -- .github/workflows/tests_and_checks.yml scripts/check_juno_v1_deployment_readme.py scripts/check_juno_v1_ci_wiring.py planning/35-deployment-readme-guard-2026-06-29.md +# no output +``` + +## Next bounded slice + +Run the deployment bundle against real uni-7 tx JSON when available, or add a tiny CI/docs guard that verifies the frontend example and README stay synchronized on required frontend address keys. diff --git a/planning/36-frontend-handoff-sync-guard-2026-06-29.md b/planning/36-frontend-handoff-sync-guard-2026-06-29.md new file mode 100644 index 000000000..5bb2ba10d --- /dev/null +++ b/planning/36-frontend-handoff-sync-guard-2026-06-29.md @@ -0,0 +1,47 @@ +# 36 — Frontend handoff sync guard (2026-06-29) + +## Decision + +Add a dependency-free guard that keeps the frontend address handoff synchronized across the deployment template, generated TypeScript declarations, consumer example, and deployment README. + +## Why + +The frontend launch surface is now spread across four files: + +- `deployment/juno-v1-testnet.template.json` — source of truth for `frontend.required_addresses` and `frontend.optional_addresses`. +- `deployment/juno-v1-frontend-config.d.ts` — generated TypeScript union consumed by frontend repos. +- `deployment/juno-v1-frontend-config.example.ts` — minimal consumer fixture and helper map. +- `deployment/README.md` — operator/frontend prose handoff. + +If any one of these drifts, a frontend can ship against the wrong contract map even when the contract artifacts are correct. This is launch risk, not scope expansion. + +## Guard + +`scripts/check_juno_v1_frontend_handoff_sync.py` verifies: + +- the deployment template frontend address keys exist in the top-level `addresses` map; +- required and optional generated TypeScript unions match the template exactly; +- the TypeScript example `required_addresses`, `optional_addresses`, and `frontendAddressMap` match the template exactly; +- the README contains the synchronized required/optional frontend address lines. + +The guard is wired into `.github/workflows/tests_and_checks.yml` before Rust setup and is itself enforced by `scripts/check_juno_v1_ci_wiring.py`. + +## Verification + +Run from repo root: + +```sh +python3 scripts/check_juno_v1_frontend_handoff_sync.py +python3 scripts/check_juno_v1_ci_wiring.py +``` + +Expected output includes: + +```text +OK: Juno v1 frontend handoff address keys are synchronized +required=4 optional=1 map_keys=5 source=deployment-template +``` + +## Scope + +Still v1 only: XYK pair creation, native incentives, no new DEX token, no stable/PCL/LST/perp/yield surface. diff --git a/planning/37-frontend-release-checklist-guard-2026-06-29.md b/planning/37-frontend-release-checklist-guard-2026-06-29.md new file mode 100644 index 000000000..6fa41f3f4 --- /dev/null +++ b/planning/37-frontend-release-checklist-guard-2026-06-29.md @@ -0,0 +1,38 @@ +# 37 — Frontend release checklist guard (2026-06-29) + +## Decision + +Add a final frontend release checklist plus a dependency-free guard for the moment real uni-7 deployment values move into the UI repo. + +## Why + +The handoff now has solid machine checks for templates, tx extraction, rendered configs, generated TypeScript, examples, and README prose. The remaining launch-risk seam is operational: copying the right files into the UI repo after real code IDs and contract addresses exist. + +A short checklist makes that seam explicit without expanding v1 scope. + +## Files + +- `deployment/frontend-release-checklist.md` — names the release files, pre-copy verification commands, synchronized frontend address surface, and v1 scope guardrails. +- `scripts/check_juno_v1_frontend_release_checklist.py` — validates the checklist against the deployment template and handoff files. + +The guard is wired into `.github/workflows/tests_and_checks.yml` before Rust setup and is enforced by `scripts/check_juno_v1_ci_wiring.py`. + +## Verification + +Run from repo root: + +```sh +python3 scripts/check_juno_v1_frontend_release_checklist.py +python3 scripts/check_juno_v1_ci_wiring.py +``` + +Expected output includes: + +```text +OK: Juno v1 frontend release checklist matches the deployment handoff +release_files=3 commands=5 required=4 optional=1 pair_discovery=factory +``` + +## Scope + +Still v1 only: copy rendered config + generated handoff type, use XYK pair-create template only as a form seed, discover pools through the factory, no new DEX token, no stable/PCL/LST/perp/yield surface. diff --git a/planning/38-dex-frontend-from-scratch-architecture-2026-07-01.md b/planning/38-dex-frontend-from-scratch-architecture-2026-07-01.md new file mode 100644 index 000000000..b08d4b09b --- /dev/null +++ b/planning/38-dex-frontend-from-scratch-architecture-2026-07-01.md @@ -0,0 +1,332 @@ +# Astroport-Core DEX Frontend From-Scratch Architecture + +Date: 2026-07-01 +Task: `t_9177c2ad` +Target repo: `JakeHartnell/astroport-core` +Target app path: `frontend/` + +## Decision + +Build the new Juno DEX frontend from scratch inside this repo under `frontend/` as an isolated Vite + React + TypeScript app. + +Why `frontend/`: + +- The repo root is a Rust/CosmWasm workspace (`Cargo.toml`) with `contracts/`, `packages/`, `schemas/`, `deployment/`, and `planning/`; a sibling `frontend/` keeps UI code separate from contracts while still versioning it with the deployment handoff. +- There is no existing `package.json`, so a standalone JS app avoids contaminating the Rust workspace and CI until the frontend lane is ready. +- `deployment/` already owns the frontend handoff (`juno-v1-frontend-config.d.ts`, release checklist, config example), so `frontend/` can consume repo-local deployment/record artifacts without moving back to `juno-website`. + +Do not revive the superseded `juno-website` implementation. Treat earlier Juno DEX docs as product input only. + +## Product frame + +V1 is a boring, trustable Juno-native DEX surface: + +- Swap verified Astroport-Juno XYK pools. +- Browse pools. +- Inspect a pool. +- Add/remove liquidity. +- Show wallet, quote, tx, explorer, and error states clearly. + +Explicit non-goals for V1: + +- no DEX token, +- no stablecoin/LST/perps/lending/vaults/launchpad, +- no stable/PCL pool launch dependency, +- no charts, volume, TVL USD, APY, rewards dashboard, or recent-trade feed unless a later indexer/API lane supplies it, +- no public liquidity recommendation; label the first pool thin-liquidity experimental. + +## Current deployment inputs + +Local `deployment/records/` is not present in this checkout, but PR #9 on `JakeHartnell/astroport-core` is open and adds: + +- `deployment/records/README.md` +- `deployment/records/juno-v1-mainnet-deployment-2026-07-01.md` + +PR #9 records a live `juno-1` Astroport-Juno v1 deployment: + +- factory: `juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca` +- first pair: `juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv` +- native coin registry: `juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2` +- router: `juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s` +- incentives: `juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598` +- oracle: `juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p` +- first pool assets: `ujuno` / `factory/juno1xsx746x4375g39f9fj07hr7qm0wuf0ksl0an76/junoagenttest202607010323` +- LP denom: `factory/juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv/astroport/share` + +The PR says factory pair, pair pool, pair simulation, router config, native registry, and smoke swap/add/withdraw checks all passed. Use these as preview/test data, not as broad public-launch copy. + +## Proposed stack + +Use: + +- Vite +- React +- TypeScript +- React Router or TanStack Router +- TanStack Query for async chain reads/mutations +- CosmJS: `@cosmjs/cosmwasm-stargate`, `@cosmjs/stargate`, `@cosmjs/proto-signing` +- Keplr wallet first; Leap can be added behind the same adapter if trivial +- CSS modules or a tiny local CSS system; defer Tailwind unless the frontend lane wants a heavier design-token setup +- Vitest + React Testing Library for pure logic/components +- Playwright only after routes exist + +Do not introduce Next/Nuxt/Storyblok. The app should be static-hostable and independent of CMS availability. + +## Route map + +- `/` — redirect or link to `/swap` inside the frontend app. +- `/swap` — default swap screen for direct XYK pair swaps. +- `/pools` — pool list from strict registry plus live pair queries. +- `/pools/:pairAddress` — pool detail, reserves, LP denom, add/remove liquidity tabs. +- `/liquidity` — wallet-centric LP overview; in V1 it may link users to pool detail pages if no wallet-position indexer exists. +- Settings should be a modal, not a route, for slippage and endpoint display. + +If the app is hosted under a larger site later, mount it at `/dex/*` with the same internal route names. + +## Frontend directory layout + +```text +frontend/ + package.json + index.html + vite.config.ts + tsconfig.json + src/ + main.tsx + app/App.tsx + app/routes.tsx + config/chains.ts + config/registry.ts + config/deployment.ts + data/registry.juno-1.json + lib/astroport/assetInfo.ts + lib/astroport/messages.ts + lib/astroport/queries.ts + lib/cosmjs/clients.ts + lib/format/amounts.ts + lib/format/addresses.ts + wallet/keplr.ts + wallet/types.ts + queries/useDexRegistry.ts + queries/usePools.ts + queries/useSwapQuote.ts + queries/useWalletBalances.ts + mutations/useSwapTx.ts + mutations/useProvideLiquidityTx.ts + mutations/useWithdrawLiquidityTx.ts + components/layout/DexShell.tsx + components/wallet/WalletConnectButton.tsx + components/wallet/ChainStatusBadge.tsx + components/swap/SwapPage.tsx + components/swap/SwapForm.tsx + components/swap/TokenSelect.tsx + components/swap/QuoteCard.tsx + components/pools/PoolsPage.tsx + components/pools/PoolTable.tsx + components/pools/PoolDetailPage.tsx + components/liquidity/AddLiquidityForm.tsx + components/liquidity/RemoveLiquidityForm.tsx + components/tx/TxStatusDialog.tsx + components/common/ExplorerLink.tsx + components/common/RiskNotice.tsx + styles/theme.css +``` + +## Registry/config source + +V1 should use a strict static registry committed under `frontend/src/data/registry.juno-1.json`, generated from deployment records when possible. + +Required registry shape: + +- top level: `chainId`, `rpcEndpoint`, `restEndpoint`, `factory`, `nativeCoinRegistry`, optional `router`, optional `incentives`, optional `oracle`, `updatedAt`, `pools[]` +- pool: `id`, `label`, `pair`, `lpToken`, `type: "xyk"`, `feeBps`, `assets`, `explorer`, `enabled`, optional `featured`, optional `notes` +- asset: `kind: "native" | "ibc" | "cw20"`, `id`, `symbol`, `decimals`, optional `denomTrace`, optional `logoURI` + +Initial preview registry can include only the PR #9 test pool, marked clearly as experimental/thin-liquidity. Do not show placeholder pools. + +Future improvement: add a repo-local generator that reads `deployment/records/*.md` or rendered deployment JSON and emits `frontend/src/data/registry.juno-1.json`, then validates no placeholder addresses or unsupported pool types are present. + +## Chain query and wallet strategy + +Read-only mode: + +- Load strict registry first. +- Query factory `pairs` and/or `pair` to verify registry pairs. +- Query pair `pool` for reserves and total share. +- Query pair `simulation` for quotes. +- Query native coin registry `native_token` / `native_tokens` for denom precision checks. +- Hide TVL, volume, APR, and charts unless separately backed by an API/indexer. + +Wallet mode: + +- Detect Keplr. +- Suggest/enable `juno-1`. +- Create `SigningCosmWasmClient` from configured RPC. +- Query native balances through Stargate/CosmWasm client. +- Direct swap: execute pair `swap` with native funds and slippage-derived `belief_price`/`max_spread` or `minimum_receive` fields supported by the schema path. +- Add liquidity: execute pair `provide_liquidity` with native funds, slippage tolerance, and optional `min_lp_to_receive` when available. +- Remove liquidity: execute pair `withdraw_liquidity`; for TokenFactory LP denoms, send the LP denom as funds if required by the contract flow. + +Router/multi-hop: + +- Keep router config available but disabled by default. +- Add router only after direct pair swaps are tested in the app. +- Use `astro_swap` operations; do not rely on `native_swap` until verified against this Juno router implementation. + +## Styling/design posture + +Direction: "Juno utility terminal," not generic DeFi casino. + +- Dark background, high-contrast panels, restrained cyan/salmon accents. +- Contract addresses, denoms, LP denom, and explorer links must be visible/copyable. +- Always show an experimental/thin-liquidity risk notice before first swap/liquidity action. +- Mobile-first swap card; pools can collapse to cards. +- Empty/error states are part of the product: registry missing, RPC degraded, no wallet, wrong network, no pair, quote failure, insufficient funds, user rejection, tx failure. + +## First implementation slices + +### Slice 1 — app skeleton and static registry + +Files: + +- create `frontend/package.json`, Vite config, TS config, `index.html` +- create `frontend/src/app/App.tsx`, `frontend/src/main.tsx`, route shell +- create `frontend/src/data/registry.juno-1.json` with the PR #9 preview pool +- create `frontend/src/config/registry.ts` with strict parsing and placeholder rejection + +Verification: + +```sh +cd frontend +npm install +npm run typecheck +npm run build +``` + +Acceptance: + +- `/swap`, `/pools`, and `/pools/:pairAddress` render from local registry data. +- Placeholder addresses fail a unit test. + +### Slice 2 — read-only contract queries + +Files: + +- `src/lib/cosmjs/clients.ts` +- `src/lib/astroport/assetInfo.ts` +- `src/lib/astroport/queries.ts` +- `src/queries/usePools.ts` +- `src/queries/useSwapQuote.ts` + +Verification: + +```sh +cd frontend +npm test +npm run typecheck +npm run build +``` + +Acceptance: + +- Pool page displays live reserves from pair `pool` or a clear RPC error. +- Swap page can quote the direct PR #9 pair through pair `simulation` in read-only mode. + +### Slice 3 — wallet connect and network guard + +Files: + +- `src/wallet/types.ts` +- `src/wallet/keplr.ts` +- `src/components/wallet/WalletConnectButton.tsx` +- `src/components/wallet/ChainStatusBadge.tsx` + +Acceptance: + +- No wallet: read-only mode remains usable. +- Wrong network: app offers `juno-1` suggest/enable recovery. +- Connected wallet shows address and native balances. + +### Slice 4 — direct swap execution + +Files: + +- `src/lib/astroport/messages.ts` +- `src/mutations/useSwapTx.ts` +- `src/components/swap/SwapForm.tsx` +- `src/components/tx/TxStatusDialog.tsx` + +Acceptance: + +- Direct native-token swap can broadcast against the preview pool. +- UI shows pending/success/failure and Mintscan tx link. +- Quote failures and slippage warnings disable submit. + +### Slice 5 — pool detail and liquidity operations + +Files: + +- `src/components/pools/PoolDetailPage.tsx` +- `src/components/liquidity/AddLiquidityForm.tsx` +- `src/components/liquidity/RemoveLiquidityForm.tsx` +- `src/mutations/useProvideLiquidityTx.ts` +- `src/mutations/useWithdrawLiquidityTx.ts` + +Acceptance: + +- Pool detail shows reserves, LP denom, and explorer links. +- Add/remove forms simulate where possible and broadcast small smoke txs against the preview pool. +- User LP balance and post-tx refresh work. + +### Slice 6 — release hardening + +Files: + +- add CI job for `frontend/` install, typecheck, tests, build +- add registry validation script or test +- add `frontend/README.md` with runbook and risk copy + +Acceptance: + +- Frontend CI runs independently of Rust contract checks. +- Registry strict mode blocks placeholders, non-`juno-1`, non-XYK V1 pools, missing explorer links, duplicate IDs, and missing required deployment addresses. + +## Open questions / blockers for implementation lane + +- Confirm package manager preference (`npm`, `pnpm`, or `yarn`). Default to npm unless the repo adopts a JS package manager standard. +- Confirm whether PR #9 deployment records will be merged before the first frontend PR. If not, copy the required public summary values into the frontend registry with a comment pointing to PR #9. +- Confirm owner/admin transfer posture before public copy: PR #9 says hot wallet owns/guards/treasury for thin-liquidity testing; broader public promotion should prefer DAO-controlled owner/admin roles. +- Confirm first counterparty asset naming: the first pool counterparty is a TokenFactory test denom, so UI copy should label it as test/preview until real launch assets are approved. + +## Evidence inspected + +Repo-local: + +- `Cargo.toml` — Rust workspace only; no frontend package exists. +- `README.md` — upstream Astroport contract repo shape. +- `deployment/README.md` — frontend consumption handoff and no-hardcoded-pools guidance. +- `deployment/juno-v1-readiness-plan.md` — XYK-only deployment and frontend readiness gates. +- `deployment/MAINNET_DEPLOYMENT.md` — mainnet/frontend handoff and smoke requirements. +- `deployment/frontend-release-checklist.md` — frontend address surface and release blockers. +- `deployment/juno-v1-frontend-config.d.ts` and `.example.ts` — generated frontend type/address contract. +- `planning/17-frontend-schema-surface-2026-06-29.md` — contract query/execute surface. +- `schemas/astroport-factory/raw/query.json` — `pair`, `pairs`, `fee_info` queries. +- `schemas/astroport-pair/raw/query.json` and `execute.json` — `pool`, `simulation`, `simulate_provide`, `simulate_withdraw`, `swap`, `provide_liquidity`, `withdraw_liquidity`. +- `schemas/astroport-router/raw/query.json` — router simulation surface. +- `schemas/astroport-native-coin-registry/raw/query.json` — denom metadata surface. + +Prior DEX notes: + +- `/opt/data/repos/juno-dex-v1-product-architecture-spec.md` +- `/opt/data/repos/juno-dex-v1-data-layer-spec.md` +- `/opt/data/repos/juno-dex-frontend-v1-design.md` + +Remote PR evidence: + +- `https://github.com/JakeHartnell/astroport-core/pull/9` — open PR adding `deployment/records/` with real mainnet deployment summary. + +## Recommended next Kanban tasks + +1. Frontend engineer: create `frontend/` Vite React skeleton and strict registry using this plan. +2. Frontend/data engineer: implement read-only CosmWasm queries against the PR #9 preview pool. +3. QA/reviewer: verify registry copy against PR #9 and smoke query pool/simulation from the UI runtime before any swap execution work. diff --git a/planning/39-dex-frontend-fullbuild-plan-2026-07-02.md b/planning/39-dex-frontend-fullbuild-plan-2026-07-02.md new file mode 100644 index 000000000..29b0d69fa --- /dev/null +++ b/planning/39-dex-frontend-fullbuild-plan-2026-07-02.md @@ -0,0 +1,205 @@ +# Juno DEX — full-build frontend plan + +Date: 2026-07-02 +Working title: **Juno DEX** +Target repo: `JakeHartnell/astroport-core` +App path: `frontend/` · new service: `services/indexer/` + +This doc is the canonical plan for taking the Juno DEX from a read-only preview +UI to a **fully featured, production-ready DEX** on top of the forked +`astroport-core` contracts. It supersedes the deliberately-minimal scope of +`38-dex-frontend-from-scratch-architecture-2026-07-01.md` (which framed V1 as a +"boring, trustable" XYK-only preview with many non-goals). The directional +decisions below were made with the owner on 2026-07-02. + +## Directive + +Build a fully featured production-ready DEX UI. **No features disabled, no +hacks or shortcuts.** Branding roughly off Juno Network (purple/indigo, Juno +logos). Draw from Interchain UI (`@interchain-ui/react`) as the component base. + +## Scope decisions (settled 2026-07-02) + +| Area | Decision | +|---|---| +| Analytics/data | **Dedicated indexer + API** workstream — real TVL, 24h volume, APR, OHLC price charts, positions, tx history. The chain only exposes current reserves; everything historical needs indexing. | +| Pool types | **All three** — XYK + Stableswap + PCL — *including deploying* the stable/PCL pair codes (only XYK code `5133` is live today). | +| Theme | **Dark-only "Juno utility terminal"** with Juno purple/indigo branding + logos. No light mode. | +| Wallets | **cosmos-kit** multi-wallet (Keplr, Leap, Cosmostation, Station, WalletConnect). Replaces the hand-rolled Keplr adapter. | +| Design system | **Interchain UI** (`@interchain-ui/react`) as the primitive/design-token base; pairs with cosmos-kit. | +| Contract clients | Typed via `@cosmwasm/ts-codegen` from committed `schemas/`. | + +## Starting point (as of this doc) + +**Contracts — done, live on `juno-1`.** Factory `juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca`, +XYK pair code `5133`, router `juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s`, +incentives `juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598`, +oracle `juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p`, +native-coin-registry `juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2`. +Permissionless pair creation is open. Test pair `juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv` +(JUNO / TokenFactory test denom). `pair_stable` + `pair_concentrated` exist +in-repo but are **not deployed**. Details: `deployment/records/juno-v1-mainnet-deployment-2026-07-01.md`. + +**Frontend — the gap.** `frontend/` is a Vite + React 19 + TS app (~1,200 LOC) +with solid chain plumbing (CosmJS, TanStack Query, live `simulation`/`pool` +reads, strict registry validation, scaffolded mutations) but **every +user-facing action is disabled or stubbed**: + +- Swap button hardcoded to `"Swap disabled: preview mode"`; slippage fixed at + 0.5% / `max_spread` "0.01"; router disabled. +- Add/Remove liquidity forms fully disabled; no LP balance queries. +- Registry has 1 pool, XYK-only, no dynamic discovery. +- Keplr-only hand-rolled adapter; ad-hoc dark CSS; no Interchain UI / cosmos-kit. +- No token logos, no TVL/volume/APR, no charts, no portfolio, no tx history. +- ~7% test coverage. + +**Approach: build on the skeleton, do not rewrite.** The plumbing is sound; +we turn features on, layer the design system + wallets, add the data layer, and +extend to all pool types. + +## Target architecture + +``` + ┌─────────────────────────────┐ + │ Juno DEX frontend (Vite/React) │ + │ @interchain-ui/react (dark) │ + │ cosmos-kit wallets │ + │ TanStack Query data layer │ + └───────┬─────────────┬───────────┘ + prefers API │ │ fallback (always works) + ▼ ▼ + ┌─────────────────────┐ ┌──────────────────────┐ + │ Indexer API │ │ juno-1 RPC/REST │ + │ TVL/vol/APR/OHLC │ │ CosmWasm queries │ + │ positions/history │ │ (reserves, quotes) │ + └──────────┬──────────┘ └──────────────────────┘ + │ ingests events + ┌──────────▼──────────┐ + │ services/indexer │ factory/pair/incentives events → Postgres + └─────────────────────┘ +``` + +Key principles: + +- **Graceful degradation.** The frontend prefers the indexer API but never + hard-breaks when it's down: core trading/liquidity fall back to direct + on-chain reads; analytics-only surfaces (charts/volume/APR) show an explicit + "unavailable" state — never fake zeros. +- **Type safety end to end.** Contract messages generated from `schemas/`. +- **Honest risk UX.** Permissionless pools → verified/unverified signaling, + thin-liquidity notices, high-price-impact confirmation. + +## Epics + +| Epic | Theme | +|---|---| +| E0 Foundations | Interchain UI + Juno dark theme, app shell, ts-codegen clients, frontend CI, shared UI kit | +| E1 Wallet | cosmos-kit multi-wallet, network guard, balances | +| E2 Swap | enable execution, slippage/price-impact, token select, reverse quote, multi-hop router, tx lifecycle | +| E3 Pools | dynamic discovery, list w/ TVL/vol/APR, detail, all pool types | +| E4 Liquidity | add/remove w/ simulation, LP positions, incentives staking/claim | +| E5 Pool creation | permissionless create + seed initial liquidity | +| E6 Portfolio | positions + rewards aggregate, tx history | +| E7 Indexer | ingestion, API, OHLC, USD pricing, frontend data layer | +| E8 Assets | chain-registry asset list + logos, verification/risk badges | +| E9 Analytics UI | price/candle charts, stats dashboard | +| E10 Contract ops | deploy stable/PCL, incentive programs + oracle, DAO ownership transfer | +| E11 Quality & release | E2E, state audit, a11y/perf, security review, hosting/CI-CD, launch checklist | + +## Milestones + +- **M1 — Usable core:** design system + brand, cosmos-kit wallets, real + direct-pair swap, add/remove liquidity, LP positions. → a working DEX on the + live pool. +- **M2 — Full trading:** multi-hop router, dynamic pool discovery, token lists + + logos, all pool types in UI **+ deploy stable/PCL**, pool creation. +- **M3 — Analytics & farming:** indexer + API, TVL/volume/APR, charts, stats + dashboard, incentives/rewards, portfolio + tx history. +- **M4 — Production hardening:** E2E + coverage, error/empty/a11y/perf, security + review, DAO ownership transfer, hosting/CI-CD, mainnet launch checklist. + +## Issue index + +GitHub milestones: `M1 — Usable core` (1), `M2 — Full trading` (2), +`M3 — Analytics & farming` (3), `M4 — Production hardening` (4). + +| # | Epic | Milestone | Title | +|---|---|---|---| +| [#12](https://github.com/JakeHartnell/astroport-core/issues/12) | E0 | M1 | Adopt @interchain-ui/react + Juno dark brand theme tokens | +| [#13](https://github.com/JakeHartnell/astroport-core/issues/13) | E0 | M1 | App shell, navigation & responsive layout on interchain-ui | +| [#14](https://github.com/JakeHartnell/astroport-core/issues/14) | E0 | M1 | Generate type-safe contract clients via ts-codegen | +| [#15](https://github.com/JakeHartnell/astroport-core/issues/15) | E0 | M1 | Frontend CI lane: typecheck, lint, test, build | +| [#16](https://github.com/JakeHartnell/astroport-core/issues/16) | E0 | M1 | Shared UI kit: token amount input, modal, toasts, skeletons, empty/error states | +| [#17](https://github.com/JakeHartnell/astroport-core/issues/17) | E1 | M1 | Integrate cosmos-kit multi-wallet; retire hand-rolled Keplr adapter | +| [#18](https://github.com/JakeHartnell/astroport-core/issues/18) | E1 | M1 | Network guard: juno-1 suggest/enable + wrong-network recovery | +| [#19](https://github.com/JakeHartnell/astroport-core/issues/19) | E1 | M1 | Wallet balances (native/IBC/LP) + address UX | +| [#20](https://github.com/JakeHartnell/astroport-core/issues/20) | E2 | M1 | Enable real direct-pair swap execution (remove preview gating) | +| [#21](https://github.com/JakeHartnell/astroport-core/issues/21) | E2 | M1 | Configurable slippage, price impact & minimum-received (settings modal) | +| [#22](https://github.com/JakeHartnell/astroport-core/issues/22) | E2 | M1 | Token selector: searchable list with logos, balances & favorites | +| [#23](https://github.com/JakeHartnell/astroport-core/issues/23) | E2 | M2 | Exact-out / reverse quoting + input debounce + quote refresh & expiry | +| [#24](https://github.com/JakeHartnell/astroport-core/issues/24) | E2 | M2 | Multi-hop routing via router contract | +| [#25](https://github.com/JakeHartnell/astroport-core/issues/25) | E2 | M1 | Swap tx lifecycle: pending/success/fail, error decoding, Mintscan links | +| [#26](https://github.com/JakeHartnell/astroport-core/issues/26) | E3 | M2 | Dynamic pool discovery from factory pairs + registry merge | +| [#27](https://github.com/JakeHartnell/astroport-core/issues/27) | E3 | M2 | Pool list: TVL / 24h volume / APR / fee tier with search, sort, filters | +| [#28](https://github.com/JakeHartnell/astroport-core/issues/28) | E3 | M2 | Pool detail page + per-pool analytics | +| [#29](https://github.com/JakeHartnell/astroport-core/issues/29) | E3 | M2 | Pool-type support in UI: XYK, Stableswap, PCL | +| [#30](https://github.com/JakeHartnell/astroport-core/issues/30) | E4 | M1 | Add liquidity: proportional + single-sided with simulation | +| [#31](https://github.com/JakeHartnell/astroport-core/issues/31) | E4 | M1 | Remove liquidity with simulation | +| [#32](https://github.com/JakeHartnell/astroport-core/issues/32) | E4 | M1 | LP position panel: balance, share %, underlying value, quick actions | +| [#33](https://github.com/JakeHartnell/astroport-core/issues/33) | E4 | M3 | Incentives: stake/unstake LP, reward APR, claim rewards | +| [#34](https://github.com/JakeHartnell/astroport-core/issues/34) | E5 | M2 | Permissionless Create Pool flow (type/assets/fee, guardrails) | +| [#35](https://github.com/JakeHartnell/astroport-core/issues/35) | E5 | M2 | Seed initial liquidity + first-provider warnings | +| [#36](https://github.com/JakeHartnell/astroport-core/issues/36) | E6 | M3 | Portfolio page: LP + staked positions, claimable rewards, aggregate value | +| [#37](https://github.com/JakeHartnell/astroport-core/issues/37) | E6 | M3 | Wallet transaction history (swaps/adds/withdraws/claims) | +| [#38](https://github.com/JakeHartnell/astroport-core/issues/38) | E7 | M3 | Stand up services/indexer: event ingestion + Postgres schema | +| [#39](https://github.com/JakeHartnell/astroport-core/issues/39) | E7 | M3 | Indexer API: TVL, volume, APR, pool stats, positions, tx history | +| [#40](https://github.com/JakeHartnell/astroport-core/issues/40) | E7 | M3 | OHLC / candle price history endpoint + backfill | +| [#41](https://github.com/JakeHartnell/astroport-core/issues/41) | E7 | M3 | USD pricing service + denom→USD resolver | +| [#42](https://github.com/JakeHartnell/astroport-core/issues/42) | E7 | M3 | Frontend data-access layer with graceful on-chain fallback | +| [#43](https://github.com/JakeHartnell/astroport-core/issues/43) | E8 | M2 | Chain-registry-backed asset list (logos, decimals, IBC denom traces) | +| [#44](https://github.com/JakeHartnell/astroport-core/issues/44) | E8 | M2 | Token verification/flagging + risk badges | +| [#45](https://github.com/JakeHartnell/astroport-core/issues/45) | E9 | M3 | Price / candle chart component (pool + swap widget) | +| [#46](https://github.com/JakeHartnell/astroport-core/issues/46) | E9 | M3 | Stats dashboard / home: protocol metrics + top pools | +| [#47](https://github.com/JakeHartnell/astroport-core/issues/47) | E10 | M2 | Deploy + register Stableswap and PCL pair codes on juno-1 | +| [#48](https://github.com/JakeHartnell/astroport-core/issues/48) | E10 | M3 | Configure incentive programs + oracle wiring + ops runbook | +| [#49](https://github.com/JakeHartnell/astroport-core/issues/49) | E10 | M4 | DAO ownership/admin transfer + production config hardening | +| [#50](https://github.com/JakeHartnell/astroport-core/issues/50) | E11 | M4 | Playwright E2E for swap / liquidity / create flows | +| [#51](https://github.com/JakeHartnell/astroport-core/issues/51) | E11 | M4 | Error / empty / loading state audit + retries | +| [#52](https://github.com/JakeHartnell/astroport-core/issues/52) | E11 | M4 | Accessibility + performance pass | +| [#53](https://github.com/JakeHartnell/astroport-core/issues/53) | E11 | M4 | Security review: frontend + indexer | +| [#54](https://github.com/JakeHartnell/astroport-core/issues/54) | E11 | M4 | Hosting + CI/CD deploy pipeline | +| [#55](https://github.com/JakeHartnell/astroport-core/issues/55) | E11 | M4 | Mainnet launch checklist + go-live runbook | + +## Critical path & sequencing + +1. **Foundations first** (#12–#16): theme + shell + ts-codegen + CI + UI kit + unblock everything. +2. **Wallets** (#17–#19) unblock all execution. +3. **M1 trading loop:** swap execution (#20, #21, #25) + add/remove/positions + (#30–#32). This yields a genuinely usable DEX on the live pool. +4. **M2 breadth:** discovery (#26) → router (#24) + pool-type UI (#29) + + assets (#43, #44) + pools list/detail (#27, #28) + creation (#34, #35). + Contract deploy (#47) gates stable/PCL in production. +5. **M3 data:** indexer (#38–#42) is the backbone for metrics/charts/portfolio + (#27, #28, #36, #37, #45, #46) and incentives APR (#33, #48). +6. **M4 launch gates:** DAO ownership (#49), security review (#53), E2E (#50), + hosting (#54), launch checklist (#55). `launch-blocker` label marks the + hard gates. + +## Launch-blocker issues + +#47 (all pool types live), #49 (DAO ownership), #53 (security review), #55 +(launch checklist) — all must clear before public promotion. Consistent with +the deployment record's recommendation to move owner/admin off the hot wallet +and keep thin-liquidity risk copy until real markets exist. + +## Notes for implementation agents + +- Every issue body carries context, the relevant contract surface (with schema + paths + live addresses), files to touch, acceptance criteria, tests, and + named dependencies. Start from the issue, not this doc. +- Labels: `epic:*` groups the workstream, `area:{frontend,indexer,contracts}` + routes the skillset, `astroport-juno` scopes the program, `launch-blocker` + marks hard gates. +- Do not re-disable features behind "preview mode." Keep honest risk UX + (verified/unverified, thin-liquidity, price-impact) instead. diff --git a/planning/40-indexer-production-readiness-2026-07-03.md b/planning/40-indexer-production-readiness-2026-07-03.md new file mode 100644 index 000000000..383f7e3b1 --- /dev/null +++ b/planning/40-indexer-production-readiness-2026-07-03.md @@ -0,0 +1,164 @@ +# Juno DEX indexer production-readiness plan + +Date: 2026-07-03 +Scope: `services/indexer/`, frontend-facing API, Postgres data model, chart/analytics data +Deployment record: `deployment/records/juno-v1-mainnet-deployment-2026-07-01.md` + +## Decisions + +- `services/indexer/` is the canonical production indexer package. +- The older `indexer/` REST skeleton may be removed once its useful API contract, tests, and mock-safety behavior are ported. +- The deployable service should expose both ingestion and HTTP API from one production service backed by Postgres. +- USD pricing comes from an external provider. When USD is unavailable, values should fall back to JUNO-denominated pricing, not fabricated USD. +- Missing USD prices must remain explicit: `priceUsd: null`, `status: "missing"` or `"stale"`, and no silent zeroes. +- The frontend API response shapes should remain compatible with `frontend/src/lib/indexer/types.ts` and `frontend/src/lib/indexer/client.ts`. + +## Juno v1 contract inputs + +Use the 2026-07-01 deployment record as source of truth: + +| Component | Value | +|---|---| +| Chain | `juno-1` | +| Factory | `juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca` | +| Router | `juno1fppwfa2efpsahvwlqprrshjth2mfqyd8n80yd7z5kpjspq30s8ksrapa8s` | +| Incentives | `juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598` | +| Oracle | `juno1szsxu32r7rnu5wq7yqlxq4x46g0fq7qpzyggcvgsh2cq554mcuqql6jw4p` | +| Native coin registry | `juno1qwer7jleluth33trk2ywqvp6vwjh4j4zar3ag6dw5d8derkpel0sq8vfh2` | +| First pair | `juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv` | +| First LP denom | `factory/juno1s0klsaye2vuueet7utec6vmyua3pq6wv8ddr2phcrgg8v9gw9r5sqvfefv/astroport/share` | + +Open input: derive the production `START_HEIGHT` from the factory instantiate/create-pair txs before the first backfill. Do not default to height `1` in production unless intentionally doing a full-chain scan. + +## Required frontend features + +The indexer must power these frontend surfaces: + +- pool list sorting/filtering by TVL, 24h volume, fees, APR, pool type, incentives; +- pool detail metrics and price/volume candles; +- stats dashboard with protocol TVL, 24h/7d volume, fees, top pools, pool count; +- swap widget market data and recent activity; +- portfolio LP positions, including unstaked and incentives-bonded LP; +- wallet transaction history for swaps, adds, withdraws, incentive bonds/unbonds/claims; +- token prices in USD when available, otherwise JUNO-relative pricing. + +## API contract + +Port the useful `indexer/` API surface into `services/indexer/`: + +- `GET /health` +- `GET /ready` +- `GET /openapi.json` +- `GET /stats` +- `GET /prices?assets=...` +- `GET /prices/:asset` +- `GET /pools?limit=&cursor=&pair=` +- `GET /pools/:id` +- `GET /pools/:id/candles?interval=5m|1h|1d&from=&to=&baseAsset="eAsset=&limit=&cursor=` +- `GET /pools/:id/positions` +- `GET /wallets/:addr/positions` +- `GET /wallets/:addr/history` + +Compatibility requirements: + +- Preserve current frontend type names and field names. +- Keep mock data opt-in only for local development and mark it with `dataSource: "mock"` and `isMock: true`. +- Empty production tables should return honest empty arrays/nulls, not mocks. +- API errors should be structured and should not leak raw DB internals. + +## Data model gaps + +The current migrations are a good base, but production API support needs these additions or confirmations: + +- asset metadata table: denom/contract, symbol, decimals, logo URI, verified status, IBC trace metadata; +- pair asset table or normalized view for fast pool lookup/filtering; +- latest pool state materialized view for reserves, total LP share, TVL in USD/JUNO, and update height; +- daily/hourly aggregate views for 24h/7d volume and fees; +- wallet transaction view combining swaps, liquidity events, and incentive events; +- LP position accounting that includes liquid LP balance and incentives-bonded LP balance; +- price table fields for both `price_usd` and `price_juno`, with source, status, observed time, and staleness. + +## Ingestion work + +1. Validate wasm event normalization against real tx fixtures from the deployment record: + - create pair; + - seed liquidity; + - smoke swap; + - smoke add liquidity; + - smoke withdraw liquidity. +2. Add tx fixture tests for each normalized event shape. +3. Backfill from the derived factory deployment height. +4. Make pool discovery fully dynamic from factory events, with registry metadata enrichment. +5. Query pair contracts after relevant events or on a scheduled cadence to capture reserves and total share. +6. Track processed block hashes and halt/alert on reorg mismatch until rollback logic is implemented. +7. Keep idempotent writes for all event rows and candle updates. + +## Pricing model + +Priority order: + +1. External USD provider for known assets. +2. JUNO-relative price from pool swaps/reserves. +3. Missing price state. + +API behavior: + +- If USD provider has a fresh price, return `priceUsd`, `status: "fresh"`, and source metadata. +- If USD provider is stale and stale values are allowed, return `status: "stale"`. +- If USD is unavailable but JUNO-relative price exists, expose JUNO-denominated fields in pool/stat calculations and leave USD fields null. +- Do not convert JUNO-relative prices into USD unless `ujuno` has a fresh/stale USD price according to provider policy. + +Implementation note: frontend types currently only model `priceUsd`. Add backward-compatible fields such as `priceJuno`, `valueJuno`, `tvlJuno`, `volume24hJuno`, and `fees24hJuno` as optional extensions before relying on them in the UI. + +## Candle/chart requirements + +- Use `token_candles` keyed by chain, pair, base asset, quote asset, interval, and bucket start. +- Support `5m`, `1h`, and `1d`. +- `open` is first trade in the bucket. +- `close` is last trade in the bucket. +- `high` and `low` are price extrema. +- `volume` is base-asset volume. +- `volumeQuote` is quote-asset volume. +- Keep USD/JUNO chart overlays separate from pair-relative OHLC candles. +- Provide a replayable candle backfill command that can rebuild a pair/range idempotently. + +## Operational readiness + +- One Docker image for migrations, poller, and API. +- Managed Postgres with backups and point-in-time recovery. +- Separate liveness/readiness semantics: + - `/health`: process is alive and reports chain/indexer lag. + - `/ready`: DB reachable, migrations applied, RPC reachable, API can serve. +- Production logs must include block ranges processed, cursor height, head height, lag, event counts, and API errors. +- Alert on: + - indexer lag above threshold; + - repeated RPC/provider failures; + - migration failure; + - DB connection saturation; + - stale price age above threshold; + - API 5xx rate; + - reorg/hash mismatch halt. + +## Implementation sequence + +1. Port `indexer/` REST API routes, response normalization, OpenAPI, and tests into `services/indexer/`. +2. Add a Postgres-backed API store in `services/indexer/`. +3. Implement `/health`, `/ready`, `/stats`, `/pools`, `/pools/:id`, and candles from Postgres. +4. Add asset metadata and USD/JUNO pricing schema changes. +5. Add wallet positions and wallet history views/endpoints. +6. Add real tx fixtures from the deployment record and harden event parsing. +7. Add reserve snapshotting and derived TVL/volume/APR materialized views. +8. Add staging seed/backfill runbook and smoke tests against the frontend. +9. Remove the old `indexer/` package after parity tests pass. +10. Wire production `VITE_DEX_INDEXER_URL` only after staging API has real non-mock data. + +## Acceptance criteria + +- `services/indexer` can run locally with Postgres, migrate, ingest, and serve the frontend API. +- Frontend can load pool list, pool detail, stats, charts, portfolio, and wallet history from the service. +- Production mode never serves mock data unless explicitly configured for dev. +- Chart candles are produced from real swaps and can be rebuilt by backfill. +- USD fields are present only when backed by external provider pricing. +- JUNO-denominated fallback fields are available when USD is missing. +- Indexer lag and DB/API health are visible from HTTP and logs. +- Old `indexer/` package is deleted or clearly deprecated after parity is complete. diff --git a/planning/41-indexer-production-architecture-2026-07-03.md b/planning/41-indexer-production-architecture-2026-07-03.md new file mode 100644 index 000000000..09c6b161e --- /dev/null +++ b/planning/41-indexer-production-architecture-2026-07-03.md @@ -0,0 +1,151 @@ +# Juno DEX Indexer Production Architecture + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** make `services/indexer/` the canonical production indexer/API service for Juno DEX: realtime ingestion, Postgres-backed analytics, frontend-compatible HTTP API, honest pricing, and observable operations. + +**Architecture:** keep the production service in TypeScript/Node for the first production cut because the repo already has a working TS ingestion foundation, Vitest tests, migrations, and frontend contract alignment. Split the code internally into ingestion, API/store, pricing, and ops modules while shipping one Docker image that can run migrations, poller, and HTTP API. Revisit Rust only after event coverage and API parity are proven; the bottleneck now is correctness/backfill semantics, not JavaScript CPU. + +**Tech Stack Recommendation:** TypeScript Node 22 + `pg` + Postgres 16+ managed service + CometBFT RPC/WebSocket + LCD contract queries + Vite/frontend existing types. Use TimescaleDB or native partitioning later if candle/history volume demands it; do not add a framework or hosted indexer dependency until the custom contract/event semantics are stable. + +--- + +## Final decision + +Ship a custom Postgres-backed TypeScript indexer first. + +| Option | Verdict | Why | +|---|---:|---| +| TypeScript Node service in `services/indexer` | **Chosen now** | Existing code, fastest path to API parity, easy frontend type sharing, enough performance for Juno DEX volume with batched RPC + Postgres indexes. | +| Rust service | Later optimization | Better CPU/memory ceiling, but higher rewrite cost before event semantics are proven. Use only if profiling shows Node is the bottleneck. | +| Go service | Not now | Good ops profile, but no repo leverage over TS and still a rewrite. | +| SubQuery/Subsquid/Hasura-first | Not now | Useful references, but contract-specific pricing/candles/reorg behavior and frontend response shapes need custom control. | +| Kafka/queue architecture | Not first cut | Adds ops surface. Add only after poller/API contention or multi-consumer needs appear. | + +## Performance/realtime model + +1. **Ingestion mode** + - Poll confirmed CometBFT blocks with `confirmationDepth >= 2` for correctness. + - Add WebSocket head subscription as an optimization for wakeups, not source of truth. + - Process contiguous height ranges with idempotent writes and cursor advancement inside one DB transaction per block. + - Track `processed_blocks(height, block_hash, parent_hash)` and halt on hash mismatch until rollback is implemented. + +2. **Backfill mode** + - Derive `START_HEIGHT` from factory instantiate/create-pair deployment transactions. + - Backfill in bounded batches by height range. + - Use replayable commands for candles/materialized aggregates. + - Never default production to height `1` unless a full-chain scan is explicitly requested. + +3. **Database model** + - Raw normalized facts: pools, swaps, liquidity events, incentive events, processed blocks. + - Latest state snapshots: reserves, total LP share, TVL USD/JUNO. + - Query surfaces: views/materialized views for latest pools, stats, wallet history, positions, hourly/daily aggregates. + - Prices store both `price_usd` and `price_juno` with explicit `status` (`fresh`, `stale`, `missing`) and source metadata. + +4. **API model** + - Expose `/health` for process/cursor/lag. + - Expose `/ready` for DB/migrations/RPC readiness. + - Keep routes compatible with `frontend/src/lib/indexer/types.ts`. + - Production empty tables return honest empty arrays/nulls. Mock data remains opt-in dev only. + - Errors are structured and do not leak raw DB internals to clients. + +## Implementation sequence + +### Task 1: Production API skeleton in `services/indexer` + +**Objective:** Port the old REST contract into the canonical package with Postgres-backed store boundaries. + +**Files:** +- Create: `services/indexer/src/api.ts` +- Create: `services/indexer/src/api-store.ts` +- Create: `services/indexer/src/openapi.ts` +- Modify: `services/indexer/src/index.ts` +- Test: `services/indexer/test/api.test.ts` + +**Verification:** +- `cd services/indexer && npm test` +- `cd services/indexer && npm run typecheck` + +### Task 2: Pricing/schema readiness + +**Objective:** Support explicit missing/stale/USD/JUNO price states without fabricated USD. + +**Files:** +- Create: `services/indexer/migrations/003_api_pricing_readiness.sql` +- Modify: `frontend/src/lib/indexer/types.ts` + +**Verification:** +- TypeScript typecheck passes. +- API price response can return `{ priceUsd: null, priceJuno: 1, status: "fresh" }`. + +### Task 3: Real tx fixtures and event hardening + +**Objective:** Lock parser correctness against deployment transactions. + +**Files:** +- Add fixtures under `services/indexer/test/fixtures/juno-v1/*.json`. +- Extend `services/indexer/test/events.test.ts`. + +**Verification:** +- create-pair, seed-liquidity, swap, add-liquidity, withdraw-liquidity fixtures normalize to stable shapes. + +### Task 4: Pool reserve snapshots + +**Objective:** Query pair contracts after relevant events and persist reserves/total share. + +**Files:** +- Modify: `services/indexer/src/rpc.ts` +- Modify: `services/indexer/src/indexer.ts` +- Modify: `services/indexer/src/db.ts` +- Test: `services/indexer/test/reserves.test.ts` + +**Verification:** +- snapshot writes are idempotent by `(pool_id,height,source)`. +- latest pool API includes reserve-backed assets. + +### Task 5: Aggregates/materialized views + +**Objective:** Serve pool list/stats from DB without scanning raw events per request. + +**Files:** +- Add migration for hourly/daily aggregates or materialized views. +- Update `PostgresApiStore.stats()` and `pools()`. + +**Verification:** +- API tests cover TVL/JUNO fallback and 24h/7d metrics. + +### Task 6: Wallet positions/history + +**Objective:** Combine LP balances, bonded balances, liquidity events, swaps, and incentives. + +**Files:** +- Add views for wallet history and positions. +- Update `PostgresApiStore.walletPositions()`, `poolPositions()`, `walletHistory()`. + +**Verification:** +- Wallet endpoints return frontend-compatible empty pages on empty DB and real rows from fixtures. + +### Task 7: Ops readiness + +**Objective:** Make the service deployable and observable. + +**Files:** +- Update `services/indexer/README.md`, `.env.example`, `Dockerfile`, `docker-compose.yml`. +- Add runbook under `deployment/`. + +**Verification:** +- local Postgres compose: migrate, run, ingest dry range, serve API. +- logs include block range, head, target, lag, event counts. + +## Current PR scope + +This PR implements Tasks 1–2: API skeleton, Postgres store boundary, `/ready`, OpenAPI parity, pricing/JUNO schema readiness, frontend optional JUNO fields, and tests. It intentionally does not claim full production ingestion/backfill is complete. + +## Acceptance criteria for production cutover + +- `services/indexer` can run migrations, ingest confirmed blocks, and serve API from the same Docker image. +- Staging API contains non-mock pool data before `VITE_DEX_INDEXER_URL` points production frontend to it. +- USD is only returned when backed by configured provider policy; otherwise USD fields are null and JUNO fields are explicit. +- Reorg mismatch halts ingestion with alerting until rollback is implemented. +- Backfill/candle rebuild commands are replayable and idempotent. +- Old `indexer/` is deleted only after parity tests pass against `services/indexer`. diff --git a/planning/42-indexer-staging-backfill-fixtures-2026-07-03.md b/planning/42-indexer-staging-backfill-fixtures-2026-07-03.md new file mode 100644 index 000000000..45ff1d1c2 --- /dev/null +++ b/planning/42-indexer-staging-backfill-fixtures-2026-07-03.md @@ -0,0 +1,40 @@ +# Juno DEX indexer staging backfill + fixture validation + +Date: 2026-07-03 +Branch: `indexer-real-fixtures-staging-readiness-2026-07-03` +Depends on: PR #99 / merge commit `752716c4fee0c56f5f58c1c27dcbd0ffab495a15` + +## Goal + +Move the indexer from API-foundation-ready to staging-backfill-ready by proving event parsing against real Juno v1 transactions, documenting the staging runbook, and tightening the first backfill gate. + +## Done criteria + +- Real public Juno v1 transaction fixtures cover: + - factory create pair; + - seed liquidity; + - smoke swap; + - smoke add liquidity; + - smoke withdraw liquidity. +- `services/indexer` tests assert normalized event outputs from those fixtures. +- Parser handles real Astroport/Juno event formats without synthetic assumptions: + - `register` factory event as the concrete pair-created source; + - comma-separated coin lists like `10000ujuno, 9803factory/...`; + - `refund_assets` and `withdrawn_share` on withdraw events. +- Staging runbook explains exact migration/backfill/API smoke flow and blockers. +- Verification passes locally and in CI before merge. + +## Non-goals for this slice + +- Do not deploy staging infra from this PR. +- Do not wire production `VITE_DEX_INDEXER_URL`. +- Do not implement reserve snapshots, pricing providers, WebSocket tailing, or rollback. +- Do not commit raw private operator archives or wasm artifacts. Fixtures are slim public tx evidence only. + +## Follow-up after this slice + +1. Add reserve snapshot worker and latest state population. +2. Add Postgres-backed aggregate rebuild commands. +3. Run the staging Postgres backfill from `START_HEIGHT=39381297`. +4. Smoke `/health`, `/ready`, `/stats`, `/pools`, `/pools/:id/candles`, `/wallets/:addr/history` against staging data. +5. Only then consider wiring frontend previews to the staging API. diff --git a/planning/43-high-performance-indexer-architecture-2026-07-04.md b/planning/43-high-performance-indexer-architecture-2026-07-04.md new file mode 100644 index 000000000..57654e536 --- /dev/null +++ b/planning/43-high-performance-indexer-architecture-2026-07-04.md @@ -0,0 +1,517 @@ +# High-performance Juno DEX indexer architecture + +Date: 2026-07-04 +Scope: replace the current serial block poller with a catch-up and realtime architecture that can index Juno v1 history as fast as the data source and Postgres allow. + +## Executive decision + +Build a two-mode indexer around a shared Postgres fact store: + +1. **Catch-up mode:** range-based, highly concurrent block acquisition; ordered block commit; bulk writes into staging tables; enrichment jobs deferred. +2. **Realtime mode:** WebSocket-triggered head tracking; confirmed-height polling; small ordered batches; enrichment workers run near-live but outside the cursor-critical path. + +The ingestion hot path must do only four things: fetch block data, normalize relevant events, persist raw facts idempotently, and advance the cursor in height order. Reserve snapshots, candles, TVL, prices, wallet aggregates, and API materialization should be independent workers that can lag, retry, and rebuild without blocking block ingestion. + +## Research notes + +- CometBFT exposes blockchain data over URI/HTTP, JSON-RPC over HTTP, and JSON-RPC over WebSockets. WebSockets support subscriptions such as `NewBlock`, but should be treated as a wakeup signal, not the source of truth for committed historical data. Source: [CometBFT RPC docs](https://docs.cosmos.network/cometbft/latest/api-reference/rpc). +- CometBFT/Tendermint event search depends on transaction and block event indexing. Defaults always include `tx.height` and `tx.hash`; richer event queries depend on node indexer configuration. Source: [Tendermint indexing transactions](https://docs.tendermint.com/v0.34/app-dev/indexing-transactions.html) and [production notes](https://docs.tendermint.com/v0.34/tendermint-core/running-in-production.html). +- Cosmos SDK exposes `GetTxsEvent` and `GetBlockWithTxs` through the tx service, including REST mappings. These are useful secondary data paths, but the current event normalizer already depends on tx result events from block results. Source: [cosmos.tx.v1beta1 service proto](https://github.com/cosmos/cosmos-sdk/blob/main/proto/cosmos/tx/v1beta1/service.proto). +- PostgreSQL bulk loading guidance is clear: `COPY` is optimized for large row loads and has less overhead than repeated `INSERT`; loading then creating indexes is fastest for fresh tables. Source: [PostgreSQL populate database docs](https://www.postgresql.org/docs/current/populate.html). +- Timescale continuous aggregates can maintain hourly/daily summaries and support manual refresh windows, but open-ended refreshes and still-hot buckets can create write amplification. Source: [Timescale continuous aggregates docs](https://github.com/timescale/docs.timescale.com-content/blob/master/using-timescaledb/continuous-aggregates.md). + +## Current bottlenecks + +The current implementation is correct but deliberately simple: + +- `Indexer.runOnce()` fetches and writes one height at a time. +- `JunoRpcClient.block()` performs two RPC requests per height: `/block` and `/block_results`. +- The DB writer loops event-by-event and swap candle writes happen inline. +- LCD reserve snapshots run after each block and are serialized by touched pair. +- `BATCH_SIZE` increases the number of heights per loop, but not concurrency. + +This means catch-up speed is roughly: + +```text +blocks_per_second ~= 1 / (block_rpc_latency + block_db_latency + reserve_snapshot_latency) +``` + +The target architecture should move to: + +```text +blocks_per_second ~= min(source_fetch_capacity, decode_capacity, ordered_commit_capacity) +``` + +## Performance target + +Initial target for staging: + +| Mode | Target | +|---|---:| +| Historical catch-up with paid archive RPC/LCD | 50-200 blocks/sec for empty/low-event ranges | +| Historical catch-up with self-hosted co-located archive RPC | 200+ blocks/sec where DB keeps up | +| Event-heavy ranges | bounded by DB upsert and candle/snapshot deferral | +| Realtime confirmed lag | under 10 confirmed blocks after catch-up | +| API p95 for pool/stats reads | under 250 ms from materialized read models | + +These are engineering targets, not guarantees. The actual ceiling will be the archive endpoint, network latency, and Postgres write IOPS. + +## Source strategy + +### Preferred production source + +Run or rent a dedicated Juno archive RPC/LCD close to the indexer and database. + +Requirements: + +- archive access back to `START_HEIGHT=39381297`; +- `/block`, `/block_results`, `/status`, and `/health`; +- WebSocket endpoint for `NewBlock`; +- height-pinned LCD smart queries for pair state snapshots; +- explicit rate limits and burst capacity; +- event indexing enabled if we choose `tx_search` or `GetTxsEvent` for targeted repair jobs. + +### Fastest source option + +Self-host a non-validator archive node in the same region/VPC as the indexer. This removes provider rate limits and Internet latency from catch-up. It also lets us tune RPC limits, connection limits, pruning/archive behavior, and tx indexing intentionally. This is the highest-ops option but the best throughput ceiling. + +### Fallback source + +Use a paid archive provider with independent RPC and LCD endpoints. Public free endpoints are acceptable for smoke tests only; they are not a viable catch-up substrate. + +## Service topology + +Use one Docker image, multiple process roles: + +| Role | Responsibility | Scales | +|---|---|---:| +| `coordinator` | owns range leases, cursors, reorg state, worker health | 1 active | +| `block-fetcher` | fetches `/block` and `/block_results` for leased ranges | horizontally | +| `decoder` | normalizes wasm events and emits compact fact batches | horizontally | +| `ordered-writer` | commits facts in contiguous height order and advances cursor | 1 per cursor | +| `snapshot-worker` | height-pinned LCD pair reserve snapshots | horizontally, rate-limited | +| `candle-worker` | rebuilds/updates OHLC buckets from swaps | horizontally by pair/range | +| `aggregate-worker` | refreshes materialized views or rollup tables | horizontally by job type | +| `api` | serves frontend-compatible HTTP API | horizontally | + +For first implementation, these can be Node processes sharing Postgres as the coordination layer. If profiling shows Node JSON parsing or event decoding is CPU-bound, rewrite `block-fetcher`/`decoder`/`ordered-writer` in Rust while keeping the API in TypeScript. + +## Data flow + +```text +confirmed target + -> range leases + -> concurrent block fetch + -> decode/normalize + -> reorder buffer by height + -> bulk stage facts + -> ordered merge into canonical tables + -> advance block cursor + -> enqueue snapshot/candle/aggregate repair jobs + -> API read models +``` + +Critical rule: only the ordered writer advances `indexer_cursors`. Everything else is replayable derived state. + +## Catch-up mode + +### Range leasing + +Add `indexer_range_leases`: + +```sql +create table indexer_range_leases ( + cursor_id text not null, + from_height bigint not null, + to_height bigint not null, + status text not null check (status in ('leased', 'fetched', 'decoded', 'committed', 'failed')), + worker_id text, + attempts integer not null default 0, + leased_until timestamptz, + error text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (cursor_id, from_height, to_height) +); +``` + +The coordinator leases contiguous chunks, for example 1,000-10,000 heights. Fetchers split leases into smaller request windows, for example 100-500 heights. + +### Concurrent fetch + +Within a fetch window: + +- fetch `/block` and `/block_results` concurrently per height; +- cap in-flight HTTP requests with `FETCH_CONCURRENCY`; +- use separate caps for RPC and LCD; +- retry transient 408/429/5xx with exponential backoff and jitter; +- keep response payloads compressed if provider supports it; +- write raw block bundles to a durable staging table or object storage for replay. + +Recommended first knobs: + +```text +RANGE_SIZE=5000 +FETCH_WINDOW_SIZE=250 +FETCH_CONCURRENCY=32 +RPC_TIMEOUT_MS=10000 +RPC_MAX_RETRIES=5 +``` + +Tune up only after measuring provider throttling and DB write saturation. + +### Ordered commit + +Fetch and decode can run ahead out of order. Commit must be contiguous: + +1. writer reads decoded batches where `height = cursor + 1`; +2. validates parent hash against `processed_blocks`; +3. writes block ledger and fact rows; +4. commits; +5. advances cursor; +6. repeats until a gap appears. + +This keeps restart and reorg behavior simple. + +### Bulk write model + +For catch-up, write to staging tables first: + +```text +stage_processed_blocks +stage_pools +stage_swaps +stage_liquidity_events +stage_incentive_events +stage_snapshot_jobs +stage_candle_jobs +``` + +Use `COPY` for large batches where possible, then merge: + +```sql +insert into swaps (...) +select ... +from stage_swaps +where batch_id = $1 +on conflict do nothing; +``` + +For smaller realtime batches, multi-row `INSERT ... ON CONFLICT` is enough. Avoid per-row insert loops in the hot path. + +### Fast catch-up switches + +Catch-up should support: + +```text +INGEST_CANDLES_INLINE=false +INGEST_RESERVE_SNAPSHOTS_INLINE=false +INGEST_AGGREGATES_INLINE=false +``` + +Default these to false for historical backfill. Derived workers repair after the raw facts are complete. + +## Realtime mode + +Realtime should subscribe to `NewBlock` over WebSocket to wake the coordinator, then process up to: + +```text +confirmed_target = head_height - CONFIRMATION_DEPTH +``` + +The actual data should still be fetched through the same block bundle path as catch-up. That avoids a separate correctness model for live blocks. + +Suggested realtime knobs: + +```text +CONFIRMATION_DEPTH=2 +REALTIME_FETCH_CONCURRENCY=8 +REALTIME_BATCH_SIZE=50 +POLL_INTERVAL_MS=1000 +``` + +If WebSocket disconnects, polling `/status` continues. WebSocket is an optimization, not a dependency. + +## Reorg handling + +Keep current conservative behavior first: halt on block hash or parent hash mismatch. + +Then implement bounded rollback: + +1. detect mismatch at height `h`; +2. find common ancestor down to `h - REORG_WINDOW`; +3. delete derived and fact rows for affected heights using partition-friendly predicates; +4. reset cursor to ancestor; +5. replay. + +Do not let snapshot/candle workers operate on heights above the committed cursor. + +Recommended settings: + +```text +CONFIRMATION_DEPTH=2 for normal realtime +REORG_WINDOW=100 for automatic rollback +MANUAL_INTERVENTION_REQUIRED beyond REORG_WINDOW +``` + +## Database architecture + +### Canonical facts + +Canonical append-mostly facts: + +- `processed_blocks(chain_id, height, block_hash, parent_hash, block_time, tx_count)` +- `pools` +- `swaps` +- `liquidity_events` +- `incentive_events` +- `pool_state_snapshots` +- `token_prices` + +### Partitioning + +Partition high-volume fact tables by height or block time: + +- `processed_blocks`: range by height; +- `swaps`, `liquidity_events`, `incentive_events`: range by block time or height; +- `token_candles`: range by bucket start; +- `pool_state_snapshots`: range by block time or height. + +Native Postgres partitioning is enough initially. Use Timescale hypertables if operationally available and we want continuous aggregates plus compression. + +### Indexing + +Keep ingestion indexes minimal: + +- uniqueness constraints needed for idempotency; +- lookup indexes used by writer hot path: `(chain_id, pair_address)` for pools; +- API indexes on read models, not necessarily raw facts. + +For a fresh historical rebuild, fastest path is: + +1. load facts into staging; +2. merge canonical rows; +3. build or rebuild nonessential indexes; +4. refresh read models. + +Do not drop production API indexes while the public API is serving traffic. Use a shadow database or maintenance window for full rebuilds. + +### Read models + +API should read from narrow, precomputed tables/views: + +- `latest_pool_state`; +- `pool_volume_windows`; +- `pool_candle_buckets`; +- `wallet_position_latest`; +- `wallet_history_flat`; +- `protocol_stats_latest`; + +For Timescale deployments, use continuous aggregates for stable hourly/daily rollups, excluding the hottest bucket where write amplification hurts. Without Timescale, maintain rollup tables with idempotent jobs keyed by `(pair_address, interval, bucket_start)`. + +## Derived workers + +### Reserve snapshots + +Snapshot jobs should be enqueued when swaps/provides/withdraws touch known pairs: + +```text +snapshot_jobs(pair_address, height, block_time, reason, status, attempts) +``` + +Workers: + +- query LCD with `x-cosmos-block-height`; +- dedupe by `(pool_id, height, source)`; +- retry transient errors; +- mark permanent failures without blocking the cursor; +- support pair/range repair jobs. + +Snapshot concurrency must be lower than block fetch concurrency because LCD smart queries are heavier and more rate-limited. + +### Candles + +Candles should be built from persisted swaps, not inline with ingestion: + +- realtime worker processes recent committed swaps every few seconds; +- catch-up worker rebuilds candles pair/range in large batches; +- writes are idempotent by `(chain_id, pair_address, asset, quote_asset, interval, bucket_start)`; +- open/close ordering must use `(height, tx_index, msg_index, event_index)` where available. + +### Wallet positions + +Wallet positions are derived from: + +- liquidity events; +- LP token balances if queried; +- incentives bond/unbond/claim events; +- optional periodic balance snapshots. + +Keep raw event facts separate from position read models so bugs in position accounting can be repaired without replaying the chain source. + +## Queue choice + +Start with Postgres-backed queues because the repo already depends on Postgres and the operational surface stays small. + +Use tables with `FOR UPDATE SKIP LOCKED` for: + +- range leases; +- snapshot jobs; +- candle jobs; +- aggregate refresh jobs. + +Introduce NATS, Kafka, or Redpanda only if: + +- workers need to scale across many hosts; +- Postgres queue contention appears in metrics; +- multiple downstream consumers need the same immutable event stream; +- replay from object storage is not enough. + +## Runtime choice + +### Phase 1 + +Keep TypeScript/Node for fastest delivery: + +- add fetch concurrency; +- add range leases; +- add staging writes; +- defer enrichment; +- preserve existing tests and API types. + +### Phase 2 + +Move the hot ingestion binary to Rust if profiling justifies it: + +- stronger typed JSON/protobuf decoding; +- lower memory overhead under high concurrency; +- better CPU throughput for large catch-up; +- still writes to the same Postgres schema. + +The API can remain TypeScript because API latency will be dominated by DB read-model queries, not CPU. + +## Observability + +Required metrics: + +- `indexer_head_height` +- `indexer_confirmed_target_height` +- `indexer_cursor_height` +- `indexer_confirmed_lag_blocks` +- `indexer_fetch_blocks_per_second` +- `indexer_fetch_rpc_requests_in_flight` +- `indexer_fetch_rpc_error_total{status}` +- `indexer_decode_blocks_per_second` +- `indexer_writer_blocks_per_second` +- `indexer_writer_commit_seconds` +- `indexer_range_lease_oldest_age_seconds` +- `indexer_snapshot_jobs_pending` +- `indexer_candle_jobs_pending` +- `indexer_db_copy_rows_total` +- `indexer_db_merge_seconds` +- `indexer_reorg_halt` + +Logs should include structured fields: + +```text +role, worker_id, range_from, range_to, height, cursor, head, target, lag, +blocks, txs, normalized_events, swaps, liquidity_events, incentive_events, +duration_ms, rpc_errors, db_duration_ms +``` + +## Recommended implementation plan + +### Task 1: Add performance config + +Add environment variables: + +```text +INDEXER_MODE=catchup|realtime +RANGE_SIZE=5000 +FETCH_WINDOW_SIZE=250 +FETCH_CONCURRENCY=32 +REALTIME_FETCH_CONCURRENCY=8 +INGEST_CANDLES_INLINE=false +INGEST_RESERVE_SNAPSHOTS_INLINE=false +INGEST_AGGREGATES_INLINE=false +``` + +### Task 2: Refactor block acquisition + +Create a block fetcher that can fetch a height range with bounded concurrency and return block bundles sorted by height. Keep current `JunoRpcClient.block(height)` as the single-height primitive. + +### Task 3: Ordered writer + +Split current `Indexer.runOnce()` into: + +- range planner; +- concurrent fetch; +- decode; +- ordered writer. + +The ordered writer should retain one transaction per block at first. After correctness tests pass, add staging-table bulk merge for catch-up. + +### Task 4: Defer reserve snapshots + +Replace inline `writeReserveSnapshots()` calls with `snapshot_jobs` inserts. Add a worker command: + +```bash +npm run worker:snapshots +``` + +### Task 5: Defer candles + +Add `candle_jobs` or make the existing `backfill:candles` range-aware and continuously runnable. Remove inline candle writes from swap insertion when `INGEST_CANDLES_INLINE=false`. + +### Task 6: Bulk staging + +Add staging tables and COPY-based loading for decoded batches. Use merge SQL into canonical tables with `ON CONFLICT DO NOTHING` or conflict-specific updates. + +### Task 7: Read models + +Move `/stats`, `/pools`, candles, wallet history, and wallet positions to precomputed read models so API traffic never scans raw event tables under load. + +### Task 8: Optional Rust ingestion + +After TypeScript pipeline metrics exist, benchmark: + +- Node fetch/decode/write; +- Rust fetch/decode/write; +- source endpoint saturation; +- Postgres merge throughput. + +Only rewrite if Node is the measured bottleneck. + +## First milestone acceptance criteria + +- Catch-up can process a 10,000-block historical range with `FETCH_CONCURRENCY > 1`. +- Blocks are committed in strict height order. +- Cursor advances only after canonical fact writes commit. +- Snapshot and candle failures cannot block block ingestion. +- Re-running the same range is idempotent. +- `/health` exposes fetch, writer, cursor, and lag metrics. +- A staging run documents measured blocks/sec, RPC error rate, DB CPU, and DB write IOPS. + +## Risks + +| Risk | Mitigation | +|---|---| +| Provider rate limits dominate | use paid archive endpoints or self-host archive node | +| Out-of-order fetch complicates reorg handling | only ordered writer advances cursor | +| Bulk writes bypass invariants | merge into canonical tables with existing unique constraints | +| Snapshot backlog grows | rate-limit, prioritize recent heights, make historical snapshots best-effort | +| Continuous aggregates amplify hot writes | exclude hottest bucket or use explicit rollup jobs | +| Postgres queue contention appears | introduce external queue only after metrics show contention | + +## Bottom line + +The fastest architecture is not a faster loop around the current serial indexer. It is a pipeline: + +- concurrent source acquisition; +- deterministic ordered commit; +- bulk database writes; +- deferred enrichment; +- precomputed API read models; +- source and database metrics driving concurrency. + +That design keeps correctness understandable while letting catch-up use all available RPC and Postgres capacity. diff --git a/planning/43-indexer-rust-hot-path-decision-2026-07-04.md b/planning/43-indexer-rust-hot-path-decision-2026-07-04.md new file mode 100644 index 000000000..6c462cfdf --- /dev/null +++ b/planning/43-indexer-rust-hot-path-decision-2026-07-04.md @@ -0,0 +1,281 @@ +# IDX-PERF-10: Rust hot-path benchmark decision record + +Date: 2026-07-04 +Issue: [#118](https://github.com/JakeHartnell/juno-dex/issues/118) +Scope: `services/indexer/` ingestion hot path only. No production Rust rewrite, TypeScript API replacement, or schema changes. + +## Decision + +**Recommendation: stay TypeScript for now.** + +The current evidence does not justify introducing Rust into the production ingestion path. The only available real Juno data in-repo is a slim set of public transaction event fixtures, and TypeScript event normalization over those fixtures is already far above expected Juno DEX block/event volume on this runner. The unmeasured and more likely bottlenecks are source RPC/LCD throughput, ordered persistence, reserve snapshot enrichment, and Postgres merge/index behavior. Rust should be reconsidered only after concurrent fetch, deferred enrichment, and bulk/staging database writes are implemented and benchmarked against captured block bundles. + +## Existing fixture data + +Real Juno v1 fixture data exists under: + +- `services/indexer/test/fixtures/juno-v1/create-pair.json` +- `services/indexer/test/fixtures/juno-v1/seed-liquidity.json` +- `services/indexer/test/fixtures/juno-v1/smoke-swap.json` +- `services/indexer/test/fixtures/juno-v1/smoke-add-liquidity.json` +- `services/indexer/test/fixtures/juno-v1/smoke-withdraw-liquidity.json` + +These fixtures are useful for parser correctness and a decode microbenchmark. They are not enough to measure the full hot path because they do not include contiguous block bundles, empty blocks, multi-tx blocks, RPC response sizes, or a staging database volume distribution. + +### Fixture gap and exact capture procedure + +Before revisiting Rust, capture a benchmark bundle that is safe to commit or store as a CI artifact: + +1. Select three height ranges from the Juno v1 deployment window: + - quiet baseline: `39381297..39381320`; + - first-pair activity: `39381305..39381360`; + - a later production/high-activity range identified from staging logs. +2. For each height, save both CometBFT endpoints: + - `GET $JUNO_RPC_URL/block?height=$HEIGHT` + - `GET $JUNO_RPC_URL/block_results?height=$HEIGHT` +3. Redact nothing from public chain data, but do not include private provider URLs, API keys, or operator archives. +4. Store as newline-delimited JSON or one JSON file per height under an isolated path such as `services/indexer/bench/fixtures/juno-v1-block-bundles/` only if the bundle is small enough for the repo; otherwise store externally and commit only a manifest with SHA256 checksums. +5. Record bundle metadata: height range, endpoint host, capture timestamp, response byte totals, block count, tx count, wasm event count, and normalized event count. + +Example capture command: + +```bash +cd services/indexer +export JUNO_RPC_URL=https:// +mkdir -p bench/fixtures/juno-v1-block-bundles +for h in $(seq 39381297 39381360); do + curl -fsS "$JUNO_RPC_URL/block?height=$h" \ + -o "bench/fixtures/juno-v1-block-bundles/$h.block.json" + curl -fsS "$JUNO_RPC_URL/block_results?height=$h" \ + -o "bench/fixtures/juno-v1-block-bundles/$h.block_results.json" +done +sha256sum bench/fixtures/juno-v1-block-bundles/*.json \ + > bench/fixtures/juno-v1-block-bundles/SHA256SUMS +``` + +## Measurements collected in this worktree + +Environment used for local measurements: + +- Node: `v22.22.3` +- Package: `services/indexer` +- Input: the five real Juno v1 transaction fixtures listed above +- Command executed from `services/indexer` after `npm ci`: + +```bash +node --expose-gc --import tsx - <<'TS' +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { normalizeBlockEvents } from './src/events.ts'; +const dir = join(process.cwd(), 'test/fixtures/juno-v1'); +const fixtures = readdirSync(dir) + .filter((f) => f.endsWith('.json')) + .map((f) => JSON.parse(readFileSync(join(dir, f), 'utf8'))); +const contracts = { + factoryAddress: 'juno1n5ettlqdt06nd346mnqy65fahcvmncaazpwn8s3m0df3ldv0d2yqjqelca', + incentivesAddress: 'juno1h0auy2knfyhkcn877cqun0fu00safgsjwvt82d4cvd0slv8q7wtsk59598', +}; +const loops = 200000; +let eventCount = 0; +let normCount = 0; +global.gc?.(); +const before = process.memoryUsage(); +const cpu0 = process.cpuUsage(); +const t0 = performance.now(); +for (let i = 0; i < loops; i += 1) { + const tx = fixtures[i % fixtures.length]; + eventCount += tx.events.length; + const normalized = normalizeBlockEvents( + tx.events, + { chainId: 'juno-1', height: tx.height, blockTime: tx.timestamp, txHash: tx.txhash }, + contracts, + ); + normCount += normalized.length; +} +const elapsed = (performance.now() - t0) / 1000; +const cpu = process.cpuUsage(cpu0); +global.gc?.(); +const after = process.memoryUsage(); +console.log(JSON.stringify({ + node: process.version, + fixtures: fixtures.length, + loops, + sourceEvents: eventCount, + normalizedEvents: normCount, + seconds: elapsed, + fixtureTxPerSec: loops / elapsed, + sourceEventsPerSec: eventCount / elapsed, + normalizedEventsPerSec: normCount / elapsed, + cpuUserSeconds: cpu.user / 1e6, + cpuSystemSeconds: cpu.system / 1e6, + rssDeltaMiB: (after.rss - before.rss) / 1048576, + heapUsedDeltaMiB: (after.heapUsed - before.heapUsed) / 1048576, +}, null, 2)); +TS +``` + +Result: + +| Metric | Value | +| --- | ---: | +| Fixture tx loops | 200,000 | +| Source fixture events decoded | 520,000 | +| Normalized events emitted | 200,000 | +| Elapsed wall time | 0.413 s | +| Fixture tx/s | 484,695 | +| Source events/s | 1,260,207 | +| Normalized events/s | 484,695 | +| CPU user time | 0.468 s | +| CPU system time | 0.000 s | +| RSS delta after GC | 1.54 MiB | +| Heap-used delta after GC | 0.11 MiB | + +Interpretation: pure TypeScript fixture decode is not currently the bottleneck. Even allowing for a large slowdown when processing full RPC block bundles, JSON parse, writes, logs, and reserve snapshots, the parser has substantial headroom relative to Juno block cadence. This microbenchmark does not prove the whole ingestion pipeline is fast; it only argues against a Rust parser rewrite before source and database measurements exist. + +## Benchmark comparison matrix + +| Area | Current result | Bottleneck read | Rust implication | +| --- | --- | --- | --- | +| TypeScript decode throughput | Measured from real tx fixtures at ~1.26M source events/s and ~485k normalized events/s. | Not a bottleneck at fixture scale. Need full block bundles for a realistic JSON parse + decode result. | Rust fetch/decode is not justified yet. | +| TypeScript fetch throughput | Attempted against default `https://rpc-juno.itastakers.com`; DNS failed in this runner with `getaddrinfo ENOTFOUND`. | No source ceiling was measured. Provider rate limits and latency are likely to dominate before parser CPU. | Do not use Rust to solve an unmeasured provider bottleneck. Add concurrent fetch/backoff first. | +| TypeScript write throughput | Not measured here because the Docker daemon was unavailable (`Cannot connect to the Docker daemon at unix:///var/run/docker.sock`) and no Postgres service was already listening on `127.0.0.1:5432`. | Ordered DB writes, indexes, and candle upserts are more likely to cap backfill throughput than decode. | A Rust writer should not be built until Postgres staging merge throughput is measured and bulk SQL shape is known. | +| Source endpoint max throughput | Not measured due DNS/network availability in this runner. | Needs provider-specific benchmark with agreed rate limits. | If source endpoint caps below target, Rust provides no benefit. | +| CPU/memory under high concurrency | Decode-only CPU was ~1.13 CPU seconds per wall second and memory delta after GC was small. High-concurrency fetch/write profile still missing. | Need `node --cpu-prof`, `--heap-prof`, and process RSS sampling during concurrent fetch + write. | Consider Rust only if profiles show sustained Node CPU/GC saturation after DB/source bottlenecks are removed. | + +## Commands and metrics to collect before revisiting + +### 1. Source endpoint maximum throughput + +Purpose: determine the block-bundle fetch ceiling for each candidate RPC provider without database writes. + +```bash +cd services/indexer +export JUNO_RPC_URL=https:// +export FROM_HEIGHT=39381297 +export TO_HEIGHT=39381360 +export CONCURRENCY=4 +node --import tsx - <<'TS' +import { performance } from 'node:perf_hooks'; +import { JunoRpcClient } from './src/rpc.ts'; +const rpc = new JunoRpcClient(process.env.JUNO_RPC_URL!); +const from = Number(process.env.FROM_HEIGHT); +const to = Number(process.env.TO_HEIGHT); +const concurrency = Number(process.env.CONCURRENCY ?? 4); +const heights = Array.from({ length: to - from + 1 }, (_, i) => from + i); +let index = 0; +let ok = 0; +let failed = 0; +let txs = 0; +let events = 0; +const t0 = performance.now(); +async function worker() { + for (;;) { + const h = heights[index++]; + if (h === undefined) return; + try { + const block = await rpc.block(h); + ok += 1; + txs += block.txCount; + events += block.txEvents.reduce((n, tx) => n + tx.events.length, 0); + } catch { + failed += 1; + } + } +} +await Promise.all(Array.from({ length: concurrency }, worker)); +const seconds = (performance.now() - t0) / 1000; +console.log({ ok, failed, seconds, blocksPerSec: ok / seconds, txs, events, eventsPerSec: events / seconds }); +TS +``` + +Collect: blocks/s, tx/s, event/s, p50/p95/p99 request latency, bytes/s, HTTP 429/5xx rate, retry count, and provider advertised limits. Repeat at concurrency `1,2,4,8,16` and stop increasing when errors or p99 latency rise sharply. + +### 2. TypeScript fetch/decode throughput without writes + +Purpose: isolate JSON decode and event normalization after source responses are available. + +```bash +cd services/indexer +node --cpu-prof --heap-prof --import tsx bench/decode-block-bundles.ts \ + --fixtures=bench/fixtures/juno-v1-block-bundles \ + --loops=100 \ + --concurrency=8 +``` + +If no benchmark helper exists yet, implement it under `services/indexer/bench/` with `tsx` only and keep it out of production build/start scripts. + +Collect: block bundles/s, source events/s, normalized events/s, CPU profile top functions, RSS peak, heap peak, GC time, and event-loop delay. + +### 3. Postgres staging merge/write throughput + +Purpose: measure ordered persistence independently from RPC. + +```bash +cd services/indexer +export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/astroport_indexer +npm run migrate +node --import tsx bench/write-normalized-events.ts \ + --fixtures=bench/fixtures/juno-v1-block-bundles \ + --mode=ordered-per-block \ + --repeat=100 +node --import tsx bench/write-normalized-events.ts \ + --fixtures=bench/fixtures/juno-v1-block-bundles \ + --mode=staging-bulk-merge \ + --repeat=100 +``` + +Collect: blocks/s, rows/s by table, transaction time p50/p95/p99, rows skipped by idempotency, Postgres CPU, WAL bytes, index size growth, lock waits, `pg_stat_statements` top queries, and connection pool utilization. + +### 4. End-to-end high-concurrency profile + +Purpose: find the actual bottleneck with realistic source, decode, ordered write, and deferred reserve snapshot behavior. + +```bash +cd services/indexer +export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/astroport_indexer +export JUNO_RPC_URL=https:// +export JUNO_REST_URL=https:// +export START_HEIGHT=39381297 +export BATCH_SIZE=100 +node --cpu-prof --heap-prof --trace-gc --import tsx src/backfill-range.ts \ + --to-height=39382300 \ + --fetch-concurrency=8 \ + --defer-reserve-snapshots=true +``` + +Collect: confirmed block lag catch-up rate, blocks/s, normalized rows/s, reserve snapshot queue depth, API responsiveness if co-hosted, RSS peak, heap peak, GC pause p99, CPU utilization per core, Postgres CPU/IO, and provider error rate. + +## Acceptance thresholds for revisiting Rust + +Revisit the decision only if all of these are true: + +1. Source RPC/LCD and Postgres have been benchmarked and are not the limiting factor for the target deployment. +2. The TypeScript implementation already uses bounded concurrent fetch, bulk/staging DB merge or equivalent batched writes, deferred reserve enrichment, and minimal synchronous logging. +3. Profiling shows Node CPU or GC is the dominant bottleneck for at least 30 minutes of sustained backfill or a representative high-traffic replay. +4. The observed performance misses target thresholds by a material margin: + - backfill target: at least **50 confirmed blocks/s** over captured fixtures or staging replay; + - live target: cursor remains within **50 confirmed blocks** of head with p95 block processing below **1 second**; + - resource target: process RSS below **1 GiB** and GC p99 pause below **100 ms** during sustained ingestion; + - reliability target: no increase in missed/retried writes or ordering violations under concurrency. +5. A Rust prototype against the same captured bundle demonstrates at least **2x end-to-end hot-path improvement** after including FFI/process-boundary overhead and operational complexity. + +## What a Rust prototype may include later + +If thresholds are missed, keep any Rust experiment isolated: + +- allowed: a standalone decoder benchmark reading captured block bundles and writing NDJSON normalized events; +- allowed: an isolated ordered-writer prototype that writes to a disposable benchmark database; +- not allowed: replacing production TypeScript API routes; +- not allowed: changing schema solely for Rust; +- not allowed: making production builds depend on Rust without a separate approved issue. + +Potential follow-up recommendations if future benchmarks justify them: + +- **move only fetch/decode to Rust** if Node CPU is dominated by JSON/RPC decode while Postgres has headroom; +- **move fetch/decode/ordered-writer to Rust** only if Node remains CPU/GC-bound after bulk staging and ordered write improvements; +- **revisit after database/source bottlenecks are removed** if provider or Postgres throughput is below target. + +## Conclusion + +Stay TypeScript for now. The current parser is fast on available real fixtures, and the missing measurements are exactly the areas Rust cannot automatically fix: RPC/LCD throughput and Postgres merge behavior. The next performance work should capture full block bundles, add isolated benchmark helpers, and measure source, decode, write, and end-to-end profiles before any production Rust work is proposed. diff --git a/planning/44-high-performance-indexer-github-issues-2026-07-04.md b/planning/44-high-performance-indexer-github-issues-2026-07-04.md new file mode 100644 index 000000000..c1df6d353 --- /dev/null +++ b/planning/44-high-performance-indexer-github-issues-2026-07-04.md @@ -0,0 +1,527 @@ +# High-performance indexer GitHub issues + +Date: 2026-07-04 +Source architecture: [43-high-performance-indexer-architecture-2026-07-04.md](./43-high-performance-indexer-architecture-2026-07-04.md) + +Use these as copy-ready GitHub issues. They are ordered so separate agents can work with minimal overlap. Start with Issues 1-4, then fan out to derived workers and read models after the ordered ingestion path is stable. + +## Dependency map + +```text +1 config + modes + -> 2 concurrent block fetcher + -> 3 ordered ingestion pipeline + -> 4 metrics + structured logs + -> 5 snapshot job queue + worker + -> 6 candle job queue + worker + -> 7 bulk staging + merge + -> 8 read models + -> 9 staging benchmark runbook + -> 10 optional Rust benchmark +``` + +--- + +## Issue 1: Add high-performance indexer runtime config and process modes + +Labels: `indexer`, `performance`, `configuration` + +Agent profile: TypeScript backend agent familiar with env parsing and tests. + +### Context + +The current indexer has only a small set of ingestion knobs: `BATCH_SIZE`, `POLL_INTERVAL_MS`, and confirmation settings. The high-performance architecture needs explicit runtime modes and separate controls for fetch concurrency, realtime concurrency, inline enrichment, and worker behavior. + +This issue should not change ingestion behavior yet. It should add validated configuration fields and documentation so later issues can consume them safely. + +### Scope + +- Extend `indexer/src/config.ts` with: + - `indexerMode: "realtime" | "catchup"`; + - `rangeSize`; + - `fetchWindowSize`; + - `fetchConcurrency`; + - `realtimeFetchConcurrency`; + - `rpcTimeoutMs`; + - `rpcMaxRetries`; + - `ingestCandlesInline`; + - `ingestReserveSnapshotsInline`; + - `ingestAggregatesInline`. +- Add defaults: + - `INDEXER_MODE=realtime`; + - `RANGE_SIZE=5000`; + - `FETCH_WINDOW_SIZE=250`; + - `FETCH_CONCURRENCY=32`; + - `REALTIME_FETCH_CONCURRENCY=8`; + - `RPC_TIMEOUT_MS=10000`; + - `RPC_MAX_RETRIES=5`; + - `INGEST_CANDLES_INLINE=true` for backward compatibility; + - `INGEST_RESERVE_SNAPSHOTS_INLINE=true` for backward compatibility; + - `INGEST_AGGREGATES_INLINE=false`. +- Validate that numeric values are non-negative and concurrency/window sizes are at least `1`. +- Validate `FETCH_CONCURRENCY <= FETCH_WINDOW_SIZE`. +- Document the new environment variables in `indexer/README.md` and `.env.example`. +- Add tests in `indexer/test/config.test.ts`. + +### Out of scope + +- No fetch concurrency implementation. +- No worker processes. +- No schema changes. + +### Acceptance criteria + +- `npm test` passes from `indexer/`. +- `npm run typecheck` passes from `indexer/`. +- Invalid `INDEXER_MODE` throws a clear config error. +- Invalid concurrency/window values throw clear config errors. +- Existing default local dev behavior remains compatible. + +--- + +## Issue 2: Implement bounded concurrent block range fetching + +Labels: `indexer`, `performance`, `rpc` + +Agent profile: TypeScript backend agent comfortable with concurrency control, retries, and deterministic tests. + +### Context + +`Indexer.runOnce()` currently processes one height at a time. `JunoRpcClient.block(height)` already fetches `/block` and `/block_results` concurrently for a single height, but the caller still serializes heights. We need a reusable range fetcher that can fetch many block bundles concurrently while returning deterministic height-ordered results. + +### Scope + +- Create `indexer/src/block-fetcher.ts`. +- Implement a function like: + +```ts +export async function fetchBlockRange(params: { + rpc: JunoRpcClient; + from: number; + to: number; + concurrency: number; +}): Promise +``` + +- Cap in-flight block fetches with the configured concurrency. +- Return block bundles sorted by ascending height. +- Fail the whole range if any height exhausts retries. +- Add retry support to `JunoRpcClient.get` using `rpcTimeoutMs` and `rpcMaxRetries` from config. +- Treat `408`, `425`, `429`, and `5xx` as transient. +- Add tests that prove: + - concurrency is greater than one; + - results are sorted even when requests resolve out of order; + - transient errors are retried; + - permanent errors fail clearly. + +### Out of scope + +- Do not change cursor advancement yet. +- Do not add staging tables. +- Do not add WebSocket behavior. + +### Acceptance criteria + +- `indexer/test/rpc.test.ts` or a new `indexer/test/block-fetcher.test.ts` covers success, sorting, concurrency cap, and retry behavior. +- Existing `JunoRpcClient.block(height)` behavior remains compatible. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 3: Refactor the indexer into fetch, decode, and ordered writer stages + +Labels: `indexer`, `performance`, `ingestion` + +Agent profile: Senior TypeScript backend agent. This issue touches the core ingestion path and should be assigned to one agent at a time. + +### Context + +Concurrent fetching only helps if the indexer can fetch ahead while preserving ordered commits. The cursor must advance only after all facts for `cursor + 1` have committed. This issue refactors `Indexer.runOnce()` into clear pipeline stages while keeping one transaction per block for correctness. + +### Scope + +- Split the current `runOnce()` logic into internal stages: + - range planning; + - block range fetch using Issue 2; + - event normalization; + - ordered block write; + - optional inline enrichment. +- Use `fetchConcurrency` in catch-up mode and `realtimeFetchConcurrency` in realtime mode. +- Preserve strict ascending commit order even if fetched blocks arrive out of order. +- Keep `recordProcessedBlock`, `writeNormalizedEvents`, and `advanceCursor` in one DB transaction per block. +- Respect `INGEST_RESERVE_SNAPSHOTS_INLINE`. + - If true, keep current snapshot behavior. + - If false, skip inline snapshots for now. Issue 5 will enqueue jobs. +- Respect `INGEST_CANDLES_INLINE` by passing a write option into DB event writing or by adding a narrowly scoped DB writer option. +- Add tests proving: + - multiple blocks are fetched before ordered writing; + - cursor advances height by height; + - a failed block write stops later cursor advancement; + - disabling inline reserve snapshots avoids LCD calls. + +### Out of scope + +- No staging tables or COPY. +- No snapshot job worker. +- No candle worker. +- No external queue. + +### Acceptance criteria + +- A bounded backfill can process more than one block per `runOnce()` while fetching with concurrency. +- Cursor advancement remains deterministic and idempotent. +- Existing reserve snapshot tests still pass, with additional coverage for disabled inline snapshots. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 4: Add ingestion throughput metrics and structured logs + +Labels: `indexer`, `observability`, `performance` + +Agent profile: Backend/observability agent. + +### Context + +We cannot tune the pipeline without measuring source throughput, writer throughput, lag, retries, and queue depth. The API already exposes Prometheus-style metrics for readiness and lag. Extend that surface with ingestion performance metrics. + +### Scope + +- Add an in-process metrics collector for the indexer process. +- Expose new metrics from `/metrics`: + - `juno_indexer_fetch_blocks_total`; + - `juno_indexer_fetch_blocks_per_second`; + - `juno_indexer_fetch_rpc_requests_in_flight`; + - `juno_indexer_fetch_rpc_error_total{status}`; + - `juno_indexer_decode_blocks_total`; + - `juno_indexer_writer_blocks_total`; + - `juno_indexer_writer_commit_seconds`; + - `juno_indexer_writer_events_total{kind}`; + - `juno_indexer_reorg_halt`. +- Add structured logs for each processed range: + +```json +{ + "role": "indexer", + "rangeFrom": 39381297, + "rangeTo": 39381355, + "cursor": 39381355, + "head": 39390000, + "target": 39389998, + "lag": 8643, + "blocks": 59, + "swaps": 2, + "liquidityEvents": 1, + "incentiveEvents": 0, + "durationMs": 1200, + "dbDurationMs": 300 +} +``` + +- Add tests for metric text output. + +### Out of scope + +- No external Prometheus integration. +- No dashboard provisioning. + +### Acceptance criteria + +- `/metrics` remains valid Prometheus text exposition. +- Existing readiness/health metrics remain unchanged. +- Logs contain enough fields to calculate blocks/sec from platform logs. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 5: Defer reserve snapshots into a Postgres-backed job queue + +Labels: `indexer`, `performance`, `database`, `worker` + +Agent profile: TypeScript backend agent comfortable with Postgres queues and idempotent workers. + +### Context + +Height-pinned LCD smart queries are expensive and currently block ingestion after each touched block. During historical catch-up, block facts should advance without waiting for reserve snapshots. This issue adds a replayable snapshot job queue and worker. + +### Scope + +- Add a migration for `snapshot_jobs`: + - `id`; + - `chain_id`; + - `pair_address`; + - `height`; + - `block_time`; + - `reason`; + - `status`; + - `attempts`; + - `leased_until`; + - `last_error`; + - timestamps. +- Add uniqueness on `(chain_id, pair_address, height, reason)`. +- When `INGEST_RESERVE_SNAPSHOTS_INLINE=false`, enqueue snapshot jobs for touched known pairs instead of querying LCD inline. +- Add `indexer/src/snapshot-worker.ts`. +- Worker behavior: + - claims jobs with `FOR UPDATE SKIP LOCKED`; + - queries LCD with `x-cosmos-block-height`; + - writes `pool_state_snapshots`; + - retries transient failures; + - marks permanent failures after max attempts. +- Add an npm script: + +```json +"worker:snapshots": "tsx src/snapshot-worker.ts" +``` + +- Add tests for enqueue idempotency, successful job processing, retry, and permanent failure. + +### Out of scope + +- No candle or aggregate jobs. +- No external queue. +- No UI/API changes. + +### Acceptance criteria + +- Disabling inline reserve snapshots no longer blocks cursor advancement. +- Snapshot jobs are idempotent and can be safely retried. +- Worker only writes snapshots for known pools. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 6: Move candle generation out of swap ingestion + +Labels: `indexer`, `performance`, `candles`, `worker` + +Agent profile: TypeScript backend agent with SQL aggregation experience. + +### Context + +Swap insertion currently updates candle rows inline for every swap and interval. That creates write amplification in the block ingestion transaction. Historical catch-up should persist swaps first, then build candles in range jobs. + +### Scope + +- Add config support from Issue 1 into DB writing so `INGEST_CANDLES_INLINE=false` skips inline candle writes. +- Add a `candle_jobs` table or make the existing candle backfill command continuously claimable by pair/range. +- Add a worker command: + +```json +"worker:candles": "tsx src/candle-worker.ts" +``` + +- Worker behavior: + - reads committed swaps by pair/range; + - computes `5m`, `1h`, and `1d` buckets; + - upserts `token_candles` idempotently; + - records job status and failures. +- Ensure open/close ordering is deterministic by height and event ordering. +- Keep the existing `backfill:candles` CLI working. +- Add tests that prove: + - inline candle writes can be disabled; + - worker rebuild produces the same candle shape as existing helpers; + - rerunning the worker is idempotent. + +### Out of scope + +- No Timescale continuous aggregates. +- No API response changes unless needed to preserve current behavior. + +### Acceptance criteria + +- Block ingestion can persist swaps without writing `token_candles`. +- Candle worker can rebuild candles from persisted swaps. +- Existing candle tests pass. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 7: Add bulk staging tables and merge path for catch-up ingestion + +Labels: `indexer`, `performance`, `database` + +Agent profile: Senior backend/database agent. This is the highest-risk performance issue and should start only after Issue 3 is stable. + +### Context + +Even with concurrent fetch, per-event inserts and per-block transactions will become the next bottleneck. PostgreSQL recommends `COPY` for large row loads. This issue adds a catch-up-only staging and merge path while preserving canonical table constraints. + +### Scope + +- Add staging tables: + - `stage_processed_blocks`; + - `stage_pools`; + - `stage_swaps`; + - `stage_liquidity_events`; + - `stage_incentive_events`. +- Add `batch_id`, `chain_id`, `height`, and timestamps to every staging row. +- Implement a catch-up batch writer: + - converts decoded events into staging rows; + - uses `COPY` or efficient multi-row insert for staging load; + - merges staging rows into canonical tables with `ON CONFLICT` behavior matching current writers; + - advances cursor only after merge succeeds. +- Keep the existing per-block ordered writer as the default until the bulk path has tests. +- Add cleanup for old successful staging batches. +- Add tests comparing canonical rows produced by: + - current per-block writer; + - new staging merge writer. + +### Out of scope + +- No dropping production indexes. +- No external object storage. +- No schema partitioning in this issue unless strictly required. + +### Acceptance criteria + +- Catch-up mode can use the staging merge path behind a config flag. +- Replaying the same batch does not duplicate canonical rows. +- Batch failure leaves cursor unchanged. +- Per-block writer remains available and tested. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 8: Build API read models for pools, stats, candles, wallet history, and positions + +Labels: `indexer`, `api`, `database`, `performance` + +Agent profile: Backend/API agent with SQL view/materialized view experience. + +### Context + +The API should not scan raw event tables under load. After ingestion is faster, API latency must come from narrow precomputed read models. This issue creates those read models and moves `PostgresApiStore` to use them where appropriate. + +### Scope + +- Add migrations for read model tables or materialized views: + - `latest_pool_state`; + - `pool_volume_windows`; + - `pool_candle_buckets` or equivalent candle API index/view; + - `wallet_history_flat`; + - `wallet_position_latest`; + - `protocol_stats_latest`. +- Add refresh SQL or worker functions for each read model. +- Update `indexer/src/api-store.ts` to prefer read models. +- Preserve frontend-compatible response shapes. +- Ensure empty production data returns honest empty arrays/nulls. +- Add tests for: + - `/stats`; + - `/pools`; + - `/pools/:id`; + - `/pools/:id/candles`; + - `/wallets/:addr/history`; + - `/wallets/:addr/positions`. + +### Out of scope + +- No USD price provider integration unless existing persisted prices are enough. +- No Timescale dependency unless already available in deployment. + +### Acceptance criteria + +- API tests prove read-model-backed responses match existing contract. +- Raw event tables are not queried for high-traffic list/stat endpoints except as fallback in tests or dev. +- `npm test` and `npm run typecheck` pass from `indexer/`. + +--- + +## Issue 9: Create staging benchmark runbook and performance acceptance test + +Labels: `indexer`, `performance`, `deployment`, `documentation` + +Agent profile: DevOps/backend agent who can write runbooks and scripts. + +### Context + +The high-performance indexer needs measured results, not theoretical throughput. This issue adds a repeatable benchmark procedure for staging and a small scripted harness that records blocks/sec, RPC failures, DB pressure, and cursor lag. + +### Scope + +- Add a runbook under `deployment/`, for example: + - `deployment/indexer-performance-benchmark-runbook.md`. +- Include required environment: + - paid or self-hosted archive RPC/LCD; + - staging Postgres; + - `INDEXER_MODE=catchup`; + - inline snapshots/candles disabled. +- Add benchmark commands for: + - 10,000-block low-event range; + - known event-heavy range; + - realtime catch-up after benchmark. +- Add SQL snippets for: + - cursor height; + - processed block count; + - swaps/liquidity count; + - staging table cleanup; + - job backlog depth. +- Add a lightweight script if useful, for example `indexer/src/benchmark-range.ts`, that runs a bounded range and prints machine-readable JSON summary. +- Document expected first milestone: + - `FETCH_CONCURRENCY > 1`; + - strict ordered cursor advancement; + - snapshot and candle workers not blocking ingestion; + - measured blocks/sec and error rates captured. + +### Out of scope + +- No platform-specific deployment automation. +- No production cutover. + +### Acceptance criteria + +- A new operator can run the benchmark from the runbook without reading architecture notes. +- The benchmark output includes block range, duration, blocks/sec, cursor, head, target, lag, RPC error count, and event counts. +- Documentation clearly states how to interpret provider throttling versus DB saturation. + +--- + +## Issue 10: Benchmark whether Rust is needed for the ingestion hot path + +Labels: `indexer`, `performance`, `research`, `rust` + +Agent profile: Performance-focused agent comfortable with TypeScript and Rust benchmarking. + +### Context + +The architecture keeps TypeScript first because the current repo and API are TypeScript. Rust should only be introduced if profiling proves Node is the bottleneck after concurrent fetch, deferred enrichment, and bulk staging are implemented. + +### Scope + +- Build a benchmark plan comparing: + - TypeScript fetch/decode/write throughput; + - source endpoint max throughput; + - Postgres staging merge throughput; + - CPU and memory profile under high concurrency. +- Optionally prototype a minimal Rust decoder/writer against fixture block bundles. +- Use existing real Juno fixture data where possible. +- Produce a markdown decision record under `planning/`. +- Recommendation must be one of: + - stay TypeScript for now; + - move only fetch/decode to Rust; + - move fetch/decode/ordered-writer to Rust; + - revisit after database/source bottlenecks are removed. + +### Out of scope + +- No production Rust rewrite. +- No replacing the TypeScript API. +- No schema changes. + +### Acceptance criteria + +- Decision record includes measured throughput and bottleneck analysis. +- Recommendation explains operational cost versus speedup. +- Any prototype is isolated and does not affect production builds unless explicitly approved later. + +--- + +## Suggested first sprint + +Assign these in parallel: + +- Agent A: Issue 1. +- Agent B: Issue 2 after Issue 1 config shape is reviewed, or with a small local config shim. +- Agent C: Issue 4, mostly independent. +- Agent D: Issue 5 schema/worker design can start after agreeing on job table names, but integration waits for Issue 3. + +Keep Issue 3 with one owner. It is the core ingestion refactor and should not be split until the fetcher and config work are merged. diff --git a/schemas/astro-token-converter/astro-token-converter.json b/schemas/astro-token-converter/astro-token-converter.json deleted file mode 100644 index 717129301..000000000 --- a/schemas/astro-token-converter/astro-token-converter.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "contract_name": "astro-token-converter", - "contract_version": "1.1.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "Instantiate message. Fields meaning is the same as in Config.", - "type": "object", - "required": [ - "new_astro_denom", - "old_astro_asset_info" - ], - "properties": { - "new_astro_denom": { - "type": "string" - }, - "old_astro_asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "outpost_burn_params": { - "anyOf": [ - { - "$ref": "#/definitions/OutpostBurnParams" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "OutpostBurnParams": { - "description": "Defines parameters for sending old IBCed ASTRO to the Hub for burning.", - "type": "object", - "required": [ - "old_astro_transfer_channel", - "terra_burn_addr" - ], - "properties": { - "old_astro_transfer_channel": { - "type": "string" - }, - "terra_burn_addr": { - "type": "string" - } - }, - "additionalProperties": false - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "Available contract execute messages. - `Convert` is used to convert old ASTRO to new ASTRO on outposts. New ASTRO sent to `receiver` if specified. - `Receive` is used to process cw20 send hook from old cw20 ASTRO and release new ASTRO token on the old Hub. Custom `receiver` is forwarded within Cw20HookMsg. - `TransferForBurning` is used to send old ASTRO to the old Hub for burning. Is meant to be used by outposts. - `Burn` is used to burn old cw20 ASTRO on the old Hub.", - "oneOf": [ - { - "type": "object", - "required": [ - "convert" - ], - "properties": { - "convert": { - "type": "object", - "properties": { - "receiver": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "transfer_for_burning" - ], - "properties": { - "transfer_for_burning": { - "type": "object", - "properties": { - "timeout": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": null, - "migrate": null, - "sudo": null, - "responses": null -} diff --git a/schemas/astro-token-converter/raw/execute.json b/schemas/astro-token-converter/raw/execute.json deleted file mode 100644 index 669d71136..000000000 --- a/schemas/astro-token-converter/raw/execute.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "Available contract execute messages. - `Convert` is used to convert old ASTRO to new ASTRO on outposts. New ASTRO sent to `receiver` if specified. - `Receive` is used to process cw20 send hook from old cw20 ASTRO and release new ASTRO token on the old Hub. Custom `receiver` is forwarded within Cw20HookMsg. - `TransferForBurning` is used to send old ASTRO to the old Hub for burning. Is meant to be used by outposts. - `Burn` is used to burn old cw20 ASTRO on the old Hub.", - "oneOf": [ - { - "type": "object", - "required": [ - "convert" - ], - "properties": { - "convert": { - "type": "object", - "properties": { - "receiver": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "transfer_for_burning" - ], - "properties": { - "transfer_for_burning": { - "type": "object", - "properties": { - "timeout": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astro-token-converter/raw/instantiate.json b/schemas/astro-token-converter/raw/instantiate.json deleted file mode 100644 index c0f09827b..000000000 --- a/schemas/astro-token-converter/raw/instantiate.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "Instantiate message. Fields meaning is the same as in Config.", - "type": "object", - "required": [ - "new_astro_denom", - "old_astro_asset_info" - ], - "properties": { - "new_astro_denom": { - "type": "string" - }, - "old_astro_asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "outpost_burn_params": { - "anyOf": [ - { - "$ref": "#/definitions/OutpostBurnParams" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "OutpostBurnParams": { - "description": "Defines parameters for sending old IBCed ASTRO to the Hub for burning.", - "type": "object", - "required": [ - "old_astro_transfer_channel", - "terra_burn_addr" - ], - "properties": { - "old_astro_transfer_channel": { - "type": "string" - }, - "terra_burn_addr": { - "type": "string" - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-incentives/astroport-incentives.json b/schemas/astroport-incentives/astroport-incentives.json index 342caba7a..cdc2c54d7 100644 --- a/schemas/astroport-incentives/astroport-incentives.json +++ b/schemas/astroport-incentives/astroport-incentives.json @@ -1,21 +1,17 @@ { "contract_name": "astroport-incentives", - "contract_version": "1.4.0", + "contract_version": "1.4.0-juno", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", "required": [ - "astro_token", "factory", "owner", - "vesting_contract" + "reward_token" ], "properties": { - "astro_token": { - "$ref": "#/definitions/AssetInfo" - }, "factory": { "type": "string" }, @@ -38,8 +34,13 @@ "owner": { "type": "string" }, - "vesting_contract": { - "type": "string" + "reward_token": { + "description": "The internal \"main emission\" reward token. Renamed from upstream's `astro_token`; Juno's deployment defaults this to native ujuno but any AssetInfo is allowed at instantiate time.", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] } }, "additionalProperties": false, @@ -211,20 +212,7 @@ "additionalProperties": false }, { - "description": "Receives a message of type [`Cw20ReceiveMsg`]. Handles cw20 LP token deposits.", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "Stake LP tokens in the Generator. LP tokens staked on behalf of recipient if recipient is set. Otherwise LP tokens are staked on behalf of message sender.", + "description": "Stake LP tokens in the Generator. LP tokens staked on behalf of recipient if recipient is set. Otherwise LP tokens are staked on behalf of message sender.\n\nAstroport-Juno only accepts token-factory LP tokens (the pair contract emits only TF LPs in this fork); the legacy cw20-LP entry point (`ExecuteMsg::Receive` + `Cw20Msg::{Deposit, DepositFor}`) was stripped in P2.5. See planning/11-incentives-and-gauges.md.", "type": "object", "required": [ "deposit" @@ -278,7 +266,7 @@ "additionalProperties": false }, { - "description": "Set a new amount of ASTRO to distribute per seconds. Only the owner can execute this.", + "description": "Set a new amount of the internal reward token to distribute per second. Only the owner can execute this.", "type": "object", "required": [ "set_tokens_per_second" @@ -291,7 +279,7 @@ ], "properties": { "amount": { - "description": "The new amount of ASTRO to distribute per second", + "description": "The new amount of the internal reward token to distribute per second", "allOf": [ { "$ref": "#/definitions/Uint128" @@ -433,7 +421,7 @@ "additionalProperties": false }, { - "description": "Update config. Only the owner can execute it.", + "description": "Update config. Only the owner can execute it.\n\nAstroport-Juno stripped `astro_token` and `vesting_contract` from the upstream UpdateConfig payload — the internal reward token is now immutable post-instantiate (a rotation requires migration) and rewards are paid directly from the incentives contract's own bank balance (the DAO refunds via BankMsg::Send).", "type": "object", "required": [ "update_config" @@ -442,24 +430,15 @@ "update_config": { "type": "object", "properties": { - "astro_token": { - "description": "The new ASTRO token info", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, + "generator_controller": { + "description": "Tristate update for the generator controller contract address. `Set(addr)` writes a new controller; `Unset` revokes the existing one (clears the binding); `NoChange` (the default) leaves the controller untouched. Both JSON field-omission AND an explicit JSON `null` decode to `NoChange` — the latter so cwgen-style TS clients that emit `null` for unset fields (rather than omitting the key) don't fail at the contract boundary. The controller — when set — is the only address other than the owner allowed to call SetupPools, and is the binding to the DAO DAO gauge adapter. An explicit `Unset` path is required so the DAO can revoke a compromised adapter without rotating ownership. See audit findings \"generator_controller unset path\" (rc2) + \"GeneratorControllerUpdate rejects explicit JSON null\" (rc3 R2).", + "default": "no_change", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/GeneratorControllerUpdate" } ] }, - "generator_controller": { - "description": "The new generator controller contract address", - "type": [ - "string", - "null" - ] - }, "guardian": { "description": "The new generator guardian", "type": [ @@ -486,13 +465,6 @@ ], "format": "uint64", "minimum": 0.0 - }, - "vesting_contract": { - "description": "The new vesting contract address", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -501,7 +473,7 @@ "additionalProperties": false }, { - "description": "Add or remove token to the block list. Only owner or guardian can execute this. Pools which contain these tokens can't be incentivized with ASTRO rewards. Also blocked tokens can't be used as external reward. Current active pools with these tokens will be removed from active set.", + "description": "Add or remove token to the block list. Only owner or guardian can execute this. Pools which contain these tokens can't be incentivized with internal rewards. Blocked tokens also can't be used as external rewards. Current active pools with these tokens will be removed from active set.", "type": "object", "required": [ "update_blocked_tokenslist" @@ -708,10 +680,6 @@ } ] }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, "Coin": { "type": "object", "required": [ @@ -727,26 +695,37 @@ } } }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "GeneratorControllerUpdate": { + "description": "Tristate update wire for the optional `generator_controller` field on `UpdateConfig`. The previous `Option` wire could only set or no-op — it had no path to revoke a controller short of rotating ownership — so a compromised gauge adapter could not be cleanly detached. This enum gives the owner an explicit `Unset` path.\n\nJSON shape (cw_serde lowercases variant names): - `{\"set\": \"\"}` — write a new controller address - `\"unset\"` — clear the controller (set to `None`) - `\"no_change\"` — leave the controller untouched (also the default when the field is omitted)", + "oneOf": [ + { + "description": "Set the controller to the given address.", + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": "string" + } + }, + "additionalProperties": false }, - "msg": { - "$ref": "#/definitions/Binary" + { + "description": "Revoke the controller (clear `Config.generator_controller`).", + "type": "string", + "enum": [ + "unset" + ] }, - "sender": { - "type": "string" + { + "description": "Leave the controller untouched. This is the default so that omitting the field from a JSON `UpdateConfig` payload behaves identically to the pre-rc3 `Option::None` semantics.", + "type": "string", + "enum": [ + "no_change" + ] } - }, - "additionalProperties": false + ] }, "IncentivizationFeeInfo": { "type": "object", @@ -1086,7 +1065,7 @@ "additionalProperties": false }, { - "description": "Returns the list of all pools receiving astro emissions", + "description": "Returns the list of all pools receiving internal emissions", "type": "object", "required": [ "active_pools" @@ -1251,30 +1230,13 @@ "title": "Config", "type": "object", "required": [ - "astro_per_second", - "astro_token", "factory", "owner", - "total_alloc_points", - "vesting_contract" + "reward_per_second", + "reward_token", + "total_alloc_points" ], "properties": { - "astro_per_second": { - "description": "Total amount of ASTRO rewards per second", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "astro_token": { - "description": "[`AssetInfo`] of the ASTRO token", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, "factory": { "description": "The Factory address", "allOf": [ @@ -1324,6 +1286,22 @@ } ] }, + "reward_per_second": { + "description": "Total amount of the internal reward token to distribute per second. Renamed from upstream's `astro_per_second`.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "reward_token": { + "description": "[`AssetInfo`] of the internal (DAO-funded) reward token. Renamed from upstream's `astro_token`; for Astroport-Juno this is typically `AssetInfo::native(\"ujuno\")`. Immutable post-instantiate.", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, "token_transfer_gas_limit": { "description": "Max allowed gas limit per one external incentive token transfer. If token transfer hits this gas limit, reward will be considered as claimed while in reality it will be stuck in the contract. If None, there is no gas limit.", "type": [ @@ -1340,14 +1318,6 @@ "$ref": "#/definitions/Uint128" } ] - }, - "vesting_contract": { - "description": "The vesting contract which distributes internal (ASTRO) rewards", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] } }, "additionalProperties": false, @@ -1742,7 +1712,7 @@ "description": "This enum is a tiny wrapper over [`AssetInfo`] to differentiate between internal and external rewards. External rewards always have a next_update_ts field which is used to update reward per second (or disable them).", "oneOf": [ { - "description": "Internal rewards aka ASTRO emissions don't have next_update_ts field and they are paid out from Vesting contract.", + "description": "Internal rewards (the DAO-funded \"main emission\" reward token; was ASTRO upstream) don't have a next_update_ts field. Astroport-Juno pays these directly from the incentives contract's own bank balance (the DAO refunds via BankMsg::Send) — upstream's vesting-contract dependency was stripped in P2.5.", "type": "object", "required": [ "int" @@ -1928,7 +1898,7 @@ "description": "This enum is a tiny wrapper over [`AssetInfo`] to differentiate between internal and external rewards. External rewards always have a next_update_ts field which is used to update reward per second (or disable them).", "oneOf": [ { - "description": "Internal rewards aka ASTRO emissions don't have next_update_ts field and they are paid out from Vesting contract.", + "description": "Internal rewards (the DAO-funded \"main emission\" reward token; was ASTRO upstream) don't have a next_update_ts field. Astroport-Juno pays these directly from the incentives contract's own bank balance (the DAO refunds via BankMsg::Send) — upstream's vesting-contract dependency was stripped in P2.5.", "type": "object", "required": [ "int" diff --git a/schemas/astroport-incentives/raw/execute.json b/schemas/astroport-incentives/raw/execute.json index efde9af48..60453d767 100644 --- a/schemas/astroport-incentives/raw/execute.json +++ b/schemas/astroport-incentives/raw/execute.json @@ -65,20 +65,7 @@ "additionalProperties": false }, { - "description": "Receives a message of type [`Cw20ReceiveMsg`]. Handles cw20 LP token deposits.", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "Stake LP tokens in the Generator. LP tokens staked on behalf of recipient if recipient is set. Otherwise LP tokens are staked on behalf of message sender.", + "description": "Stake LP tokens in the Generator. LP tokens staked on behalf of recipient if recipient is set. Otherwise LP tokens are staked on behalf of message sender.\n\nAstroport-Juno only accepts token-factory LP tokens (the pair contract emits only TF LPs in this fork); the legacy cw20-LP entry point (`ExecuteMsg::Receive` + `Cw20Msg::{Deposit, DepositFor}`) was stripped in P2.5. See planning/11-incentives-and-gauges.md.", "type": "object", "required": [ "deposit" @@ -132,7 +119,7 @@ "additionalProperties": false }, { - "description": "Set a new amount of ASTRO to distribute per seconds. Only the owner can execute this.", + "description": "Set a new amount of the internal reward token to distribute per second. Only the owner can execute this.", "type": "object", "required": [ "set_tokens_per_second" @@ -145,7 +132,7 @@ ], "properties": { "amount": { - "description": "The new amount of ASTRO to distribute per second", + "description": "The new amount of the internal reward token to distribute per second", "allOf": [ { "$ref": "#/definitions/Uint128" @@ -287,7 +274,7 @@ "additionalProperties": false }, { - "description": "Update config. Only the owner can execute it.", + "description": "Update config. Only the owner can execute it.\n\nAstroport-Juno stripped `astro_token` and `vesting_contract` from the upstream UpdateConfig payload — the internal reward token is now immutable post-instantiate (a rotation requires migration) and rewards are paid directly from the incentives contract's own bank balance (the DAO refunds via BankMsg::Send).", "type": "object", "required": [ "update_config" @@ -296,24 +283,15 @@ "update_config": { "type": "object", "properties": { - "astro_token": { - "description": "The new ASTRO token info", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, + "generator_controller": { + "description": "Tristate update for the generator controller contract address. `Set(addr)` writes a new controller; `Unset` revokes the existing one (clears the binding); `NoChange` (the default) leaves the controller untouched. Both JSON field-omission AND an explicit JSON `null` decode to `NoChange` — the latter so cwgen-style TS clients that emit `null` for unset fields (rather than omitting the key) don't fail at the contract boundary. The controller — when set — is the only address other than the owner allowed to call SetupPools, and is the binding to the DAO DAO gauge adapter. An explicit `Unset` path is required so the DAO can revoke a compromised adapter without rotating ownership. See audit findings \"generator_controller unset path\" (rc2) + \"GeneratorControllerUpdate rejects explicit JSON null\" (rc3 R2).", + "default": "no_change", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/GeneratorControllerUpdate" } ] }, - "generator_controller": { - "description": "The new generator controller contract address", - "type": [ - "string", - "null" - ] - }, "guardian": { "description": "The new generator guardian", "type": [ @@ -340,13 +318,6 @@ ], "format": "uint64", "minimum": 0.0 - }, - "vesting_contract": { - "description": "The new vesting contract address", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -355,7 +326,7 @@ "additionalProperties": false }, { - "description": "Add or remove token to the block list. Only owner or guardian can execute this. Pools which contain these tokens can't be incentivized with ASTRO rewards. Also blocked tokens can't be used as external reward. Current active pools with these tokens will be removed from active set.", + "description": "Add or remove token to the block list. Only owner or guardian can execute this. Pools which contain these tokens can't be incentivized with internal rewards. Blocked tokens also can't be used as external rewards. Current active pools with these tokens will be removed from active set.", "type": "object", "required": [ "update_blocked_tokenslist" @@ -562,10 +533,6 @@ } ] }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, "Coin": { "type": "object", "required": [ @@ -581,26 +548,37 @@ } } }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "GeneratorControllerUpdate": { + "description": "Tristate update wire for the optional `generator_controller` field on `UpdateConfig`. The previous `Option` wire could only set or no-op — it had no path to revoke a controller short of rotating ownership — so a compromised gauge adapter could not be cleanly detached. This enum gives the owner an explicit `Unset` path.\n\nJSON shape (cw_serde lowercases variant names): - `{\"set\": \"\"}` — write a new controller address - `\"unset\"` — clear the controller (set to `None`) - `\"no_change\"` — leave the controller untouched (also the default when the field is omitted)", + "oneOf": [ + { + "description": "Set the controller to the given address.", + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": "string" + } + }, + "additionalProperties": false }, - "msg": { - "$ref": "#/definitions/Binary" + { + "description": "Revoke the controller (clear `Config.generator_controller`).", + "type": "string", + "enum": [ + "unset" + ] }, - "sender": { - "type": "string" + { + "description": "Leave the controller untouched. This is the default so that omitting the field from a JSON `UpdateConfig` payload behaves identically to the pre-rc3 `Option::None` semantics.", + "type": "string", + "enum": [ + "no_change" + ] } - }, - "additionalProperties": false + ] }, "IncentivizationFeeInfo": { "type": "object", diff --git a/schemas/astroport-incentives/raw/instantiate.json b/schemas/astroport-incentives/raw/instantiate.json index c891dc515..9c16d5c50 100644 --- a/schemas/astroport-incentives/raw/instantiate.json +++ b/schemas/astroport-incentives/raw/instantiate.json @@ -3,15 +3,11 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "astro_token", "factory", "owner", - "vesting_contract" + "reward_token" ], "properties": { - "astro_token": { - "$ref": "#/definitions/AssetInfo" - }, "factory": { "type": "string" }, @@ -34,8 +30,13 @@ "owner": { "type": "string" }, - "vesting_contract": { - "type": "string" + "reward_token": { + "description": "The internal \"main emission\" reward token. Renamed from upstream's `astro_token`; Juno's deployment defaults this to native ujuno but any AssetInfo is allowed at instantiate time.", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] } }, "additionalProperties": false, diff --git a/schemas/astroport-incentives/raw/query.json b/schemas/astroport-incentives/raw/query.json index 1fb73080c..b75d6395e 100644 --- a/schemas/astroport-incentives/raw/query.json +++ b/schemas/astroport-incentives/raw/query.json @@ -286,7 +286,7 @@ "additionalProperties": false }, { - "description": "Returns the list of all pools receiving astro emissions", + "description": "Returns the list of all pools receiving internal emissions", "type": "object", "required": [ "active_pools" diff --git a/schemas/astroport-incentives/raw/response_to_config.json b/schemas/astroport-incentives/raw/response_to_config.json index 2b12857d8..cc1d22c18 100644 --- a/schemas/astroport-incentives/raw/response_to_config.json +++ b/schemas/astroport-incentives/raw/response_to_config.json @@ -3,30 +3,13 @@ "title": "Config", "type": "object", "required": [ - "astro_per_second", - "astro_token", "factory", "owner", - "total_alloc_points", - "vesting_contract" + "reward_per_second", + "reward_token", + "total_alloc_points" ], "properties": { - "astro_per_second": { - "description": "Total amount of ASTRO rewards per second", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "astro_token": { - "description": "[`AssetInfo`] of the ASTRO token", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, "factory": { "description": "The Factory address", "allOf": [ @@ -76,6 +59,22 @@ } ] }, + "reward_per_second": { + "description": "Total amount of the internal reward token to distribute per second. Renamed from upstream's `astro_per_second`.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "reward_token": { + "description": "[`AssetInfo`] of the internal (DAO-funded) reward token. Renamed from upstream's `astro_token`; for Astroport-Juno this is typically `AssetInfo::native(\"ujuno\")`. Immutable post-instantiate.", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + }, "token_transfer_gas_limit": { "description": "Max allowed gas limit per one external incentive token transfer. If token transfer hits this gas limit, reward will be considered as claimed while in reality it will be stuck in the contract. If None, there is no gas limit.", "type": [ @@ -92,14 +91,6 @@ "$ref": "#/definitions/Uint128" } ] - }, - "vesting_contract": { - "description": "The vesting contract which distributes internal (ASTRO) rewards", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] } }, "additionalProperties": false, diff --git a/schemas/astroport-incentives/raw/response_to_pool_info.json b/schemas/astroport-incentives/raw/response_to_pool_info.json index 74b4e4a69..1cccbdfe9 100644 --- a/schemas/astroport-incentives/raw/response_to_pool_info.json +++ b/schemas/astroport-incentives/raw/response_to_pool_info.json @@ -137,7 +137,7 @@ "description": "This enum is a tiny wrapper over [`AssetInfo`] to differentiate between internal and external rewards. External rewards always have a next_update_ts field which is used to update reward per second (or disable them).", "oneOf": [ { - "description": "Internal rewards aka ASTRO emissions don't have next_update_ts field and they are paid out from Vesting contract.", + "description": "Internal rewards (the DAO-funded \"main emission\" reward token; was ASTRO upstream) don't have a next_update_ts field. Astroport-Juno pays these directly from the incentives contract's own bank balance (the DAO refunds via BankMsg::Send) — upstream's vesting-contract dependency was stripped in P2.5.", "type": "object", "required": [ "int" diff --git a/schemas/astroport-incentives/raw/response_to_reward_info.json b/schemas/astroport-incentives/raw/response_to_reward_info.json index 7dee6a0fa..6f96642f1 100644 --- a/schemas/astroport-incentives/raw/response_to_reward_info.json +++ b/schemas/astroport-incentives/raw/response_to_reward_info.json @@ -111,7 +111,7 @@ "description": "This enum is a tiny wrapper over [`AssetInfo`] to differentiate between internal and external rewards. External rewards always have a next_update_ts field which is used to update reward per second (or disable them).", "oneOf": [ { - "description": "Internal rewards aka ASTRO emissions don't have next_update_ts field and they are paid out from Vesting contract.", + "description": "Internal rewards (the DAO-funded \"main emission\" reward token; was ASTRO upstream) don't have a next_update_ts field. Astroport-Juno pays these directly from the incentives contract's own bank balance (the DAO refunds via BankMsg::Send) — upstream's vesting-contract dependency was stripped in P2.5.", "type": "object", "required": [ "int" diff --git a/schemas/astroport-maker/astroport-maker.json b/schemas/astroport-maker/astroport-maker.json deleted file mode 100644 index c0b0fd6d3..000000000 --- a/schemas/astroport-maker/astroport-maker.json +++ /dev/null @@ -1,1409 +0,0 @@ -{ - "contract_name": "astroport-maker", - "contract_version": "1.7.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure stores general parameters for the contract.", - "type": "object", - "required": [ - "astro_token", - "factory_contract", - "owner" - ], - "properties": { - "astro_token": { - "description": "The ASTRO token asset info", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "collect_cooldown": { - "description": "If set defines the period when maker collect can be called", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "default_bridge": { - "description": "Default bridge asset (Terra1 - LUNC, Terra2 - LUNA, etc.)", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "factory_contract": { - "description": "The factory contract address", - "type": "string" - }, - "governance_contract": { - "description": "The governance contract address (fee distributor for vxASTRO)", - "type": [ - "string", - "null" - ] - }, - "governance_percent": { - "description": "The percentage of fees that go to governance_contract", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "description": "The maximum spread used when swapping fee tokens to ASTRO", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "owner": { - "description": "Address that's allowed to change contract parameters", - "type": "string" - }, - "second_receiver_params": { - "description": "The second receiver parameters of fees", - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverParams" - }, - { - "type": "null" - } - ] - }, - "staking_contract": { - "description": "The xASTRO staking contract address. If None then governance_contract must be set with 100% fee.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "SecondReceiverParams": { - "description": "This structure describes the parameters for updating the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver", - "type": "string" - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the functions that can be executed in this contract.", - "oneOf": [ - { - "description": "Collects and swaps fee tokens to ASTRO", - "type": "object", - "required": [ - "collect" - ], - "properties": { - "collect": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets to swap to ASTRO", - "type": "array", - "items": { - "$ref": "#/definitions/AssetWithLimit" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Updates general settings", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "properties": { - "astro_token": { - "description": "The ASTRO token asset info", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "basic_asset": { - "description": "Basic chain asset (Terra1 - LUNC, Terra2 - LUNA, etc.)", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "collect_cooldown": { - "description": "Defines the period when maker collect can be called", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "dev_fund_config": { - "description": "Dev tax configuration", - "anyOf": [ - { - "$ref": "#/definitions/UpdateDevFundConfig" - }, - { - "type": "null" - } - ] - }, - "factory_contract": { - "description": "The factory contract address", - "type": [ - "string", - "null" - ] - }, - "governance_contract": { - "description": "The governance contract address (fee distributor for vxASTRO)", - "anyOf": [ - { - "$ref": "#/definitions/UpdateAddr" - }, - { - "type": "null" - } - ] - }, - "governance_percent": { - "description": "The percentage of fees that go to governance_contract", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "description": "The maximum spread used when swapping fee tokens to ASTRO", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "second_receiver_params": { - "description": "The second receiver parameters of fees", - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverParams" - }, - { - "type": "null" - } - ] - }, - "staking_contract": { - "description": "The xASTRO staking contract address", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Add bridge tokens used to swap specific fee tokens to ASTRO (effectively declaring a swap route)", - "type": "object", - "required": [ - "update_bridges" - ], - "properties": { - "update_bridges": { - "type": "object", - "properties": { - "add": { - "type": [ - "array", - "null" - ], - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "remove": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap fee tokens via bridge assets", - "type": "object", - "required": [ - "swap_bridge_assets" - ], - "properties": { - "swap_bridge_assets": { - "type": "object", - "required": [ - "assets", - "depth" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "depth": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Distribute ASTRO to stakers and to governance", - "type": "object", - "required": [ - "distribute_astro" - ], - "properties": { - "distribute_astro": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Creates a request to change the contract's ownership", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The validity period of the proposal to change the owner", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "The newly proposed owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes a request to change contract ownership", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Claims contract ownership", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Enables the distribution of current fees accrued in the contract over \"blocks\" number of blocks", - "type": "object", - "required": [ - "enable_rewards" - ], - "properties": { - "enable_rewards": { - "type": "object", - "required": [ - "blocks" - ], - "properties": { - "blocks": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Permissionless endpoint that sends certain assets to predefined seizing address", - "type": "object", - "required": [ - "seize" - ], - "properties": { - "seize": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets to seize", - "type": "array", - "items": { - "$ref": "#/definitions/AssetWithLimit" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Sets parameters for seizing assets. Permissioned to a contract owner. If governance wants to stop seizing assets, it can set an empty list of seizable assets.", - "type": "object", - "required": [ - "update_seize_config" - ], - "properties": { - "update_seize_config": { - "type": "object", - "properties": { - "receiver": { - "description": "The address that will receive the seized tokens", - "type": [ - "string", - "null" - ] - }, - "seizable_assets": { - "description": "The assets that can be seized. Resets the list to this one every time it is executed", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "AssetWithLimit": { - "description": "This struct holds parameters to help with swapping a specific amount of a fee token to ASTRO.", - "type": "object", - "required": [ - "info" - ], - "properties": { - "info": { - "description": "Information about the fee token to swap", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "limit": { - "description": "The amount of tokens to swap", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "DevFundConfig": { - "type": "object", - "required": [ - "address", - "asset_info", - "share" - ], - "properties": { - "address": { - "description": "The dev fund address", - "type": "string" - }, - "asset_info": { - "description": "Asset that devs want ASTRO to be swapped to", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "share": { - "description": "The percentage of fees that go to the dev fund", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - } - }, - "additionalProperties": false - }, - "SecondReceiverParams": { - "description": "This structure describes the parameters for updating the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver", - "type": "string" - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - }, - "UpdateAddr": { - "description": "This is an enum used for setting and removing a contract address.", - "oneOf": [ - { - "description": "Sets a new contract address.", - "type": "object", - "required": [ - "set" - ], - "properties": { - "set": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "Removes a contract address.", - "type": "object", - "required": [ - "remove" - ], - "properties": { - "remove": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "UpdateDevFundConfig": { - "type": "object", - "properties": { - "set": { - "description": "If 'set' is None then dev fund config will be removed, otherwise it will be updated with the new parameters", - "anyOf": [ - { - "$ref": "#/definitions/DevFundConfig" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query functions available in the contract.", - "oneOf": [ - { - "description": "Returns information about the maker configs that contains in the [`ConfigResponse`]", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance for each asset in the specified input parameters", - "type": "object", - "required": [ - "balances" - ], - "properties": { - "balances": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "bridges" - ], - "properties": { - "bridges": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the seize config", - "type": "object", - "required": [ - "query_seize_config" - ], - "properties": { - "query_seize_config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message.", - "type": "object", - "properties": { - "collect_cooldown": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "second_receiver_params": { - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverParams" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "SecondReceiverParams": { - "description": "This structure describes the parameters for updating the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver", - "type": "string" - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "sudo": null, - "responses": { - "balances": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalancesResponse", - "description": "A custom struct used to return multiple asset balances.", - "type": "object", - "required": [ - "balances" - ], - "properties": { - "balances": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "bridges": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Tuple_of_String_and_String", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "A custom struct that holds contract parameters and is used to retrieve them.", - "type": "object", - "required": [ - "astro_token", - "factory_contract", - "governance_percent", - "max_spread", - "owner", - "pre_upgrade_astro_amount", - "remainder_reward" - ], - "properties": { - "astro_token": { - "description": "The ASTRO token asset info", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "default_bridge": { - "description": "Default bridge (Terra1 - LUNC, Terra2 - LUNA, etc.)", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "dev_fund_conf": { - "description": "The dev fund configuration", - "anyOf": [ - { - "$ref": "#/definitions/DevFundConfig" - }, - { - "type": "null" - } - ] - }, - "factory_contract": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "governance_contract": { - "description": "The governance contract address (fee distributor for vxASTRO stakers)", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "governance_percent": { - "description": "The percentage of fees that go to governance_contract", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "max_spread": { - "description": "The maximum spread used when swapping fee tokens to ASTRO", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "owner": { - "description": "Address that is allowed to update contract parameters", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "pre_upgrade_astro_amount": { - "description": "The amount of ASTRO tokens accrued before upgrading the Maker implementation and enabling reward distribution", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "remainder_reward": { - "description": "The remainder ASTRO tokens (accrued before the Maker is upgraded) to be distributed to xASTRO stakers", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "second_receiver_cfg": { - "description": "Parameters that describe the second receiver of fees", - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverConfig" - }, - { - "type": "null" - } - ] - }, - "staking_contract": { - "description": "The xASTRO staking contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "DevFundConfig": { - "type": "object", - "required": [ - "address", - "asset_info", - "share" - ], - "properties": { - "address": { - "description": "The dev fund address", - "type": "string" - }, - "asset_info": { - "description": "Asset that devs want ASTRO to be swapped to", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "share": { - "description": "The percentage of fees that go to the dev fund", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - } - }, - "additionalProperties": false - }, - "SecondReceiverConfig": { - "description": "This structure stores the parameters for the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "query_seize_config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SeizeConfig", - "type": "object", - "required": [ - "receiver", - "seizable_assets" - ], - "properties": { - "receiver": { - "description": "The address of the contract that will receive the seized tokens", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "seizable_assets": { - "description": "The assets that can be seized", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } - } - } -} diff --git a/schemas/astroport-maker/raw/execute.json b/schemas/astroport-maker/raw/execute.json deleted file mode 100644 index d14e4cfb6..000000000 --- a/schemas/astroport-maker/raw/execute.json +++ /dev/null @@ -1,576 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the functions that can be executed in this contract.", - "oneOf": [ - { - "description": "Collects and swaps fee tokens to ASTRO", - "type": "object", - "required": [ - "collect" - ], - "properties": { - "collect": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets to swap to ASTRO", - "type": "array", - "items": { - "$ref": "#/definitions/AssetWithLimit" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Updates general settings", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "properties": { - "astro_token": { - "description": "The ASTRO token asset info", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "basic_asset": { - "description": "Basic chain asset (Terra1 - LUNC, Terra2 - LUNA, etc.)", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "collect_cooldown": { - "description": "Defines the period when maker collect can be called", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "dev_fund_config": { - "description": "Dev tax configuration", - "anyOf": [ - { - "$ref": "#/definitions/UpdateDevFundConfig" - }, - { - "type": "null" - } - ] - }, - "factory_contract": { - "description": "The factory contract address", - "type": [ - "string", - "null" - ] - }, - "governance_contract": { - "description": "The governance contract address (fee distributor for vxASTRO)", - "anyOf": [ - { - "$ref": "#/definitions/UpdateAddr" - }, - { - "type": "null" - } - ] - }, - "governance_percent": { - "description": "The percentage of fees that go to governance_contract", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "description": "The maximum spread used when swapping fee tokens to ASTRO", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "second_receiver_params": { - "description": "The second receiver parameters of fees", - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverParams" - }, - { - "type": "null" - } - ] - }, - "staking_contract": { - "description": "The xASTRO staking contract address", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Add bridge tokens used to swap specific fee tokens to ASTRO (effectively declaring a swap route)", - "type": "object", - "required": [ - "update_bridges" - ], - "properties": { - "update_bridges": { - "type": "object", - "properties": { - "add": { - "type": [ - "array", - "null" - ], - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "remove": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap fee tokens via bridge assets", - "type": "object", - "required": [ - "swap_bridge_assets" - ], - "properties": { - "swap_bridge_assets": { - "type": "object", - "required": [ - "assets", - "depth" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "depth": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Distribute ASTRO to stakers and to governance", - "type": "object", - "required": [ - "distribute_astro" - ], - "properties": { - "distribute_astro": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Creates a request to change the contract's ownership", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The validity period of the proposal to change the owner", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "The newly proposed owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes a request to change contract ownership", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Claims contract ownership", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Enables the distribution of current fees accrued in the contract over \"blocks\" number of blocks", - "type": "object", - "required": [ - "enable_rewards" - ], - "properties": { - "enable_rewards": { - "type": "object", - "required": [ - "blocks" - ], - "properties": { - "blocks": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Permissionless endpoint that sends certain assets to predefined seizing address", - "type": "object", - "required": [ - "seize" - ], - "properties": { - "seize": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets to seize", - "type": "array", - "items": { - "$ref": "#/definitions/AssetWithLimit" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Sets parameters for seizing assets. Permissioned to a contract owner. If governance wants to stop seizing assets, it can set an empty list of seizable assets.", - "type": "object", - "required": [ - "update_seize_config" - ], - "properties": { - "update_seize_config": { - "type": "object", - "properties": { - "receiver": { - "description": "The address that will receive the seized tokens", - "type": [ - "string", - "null" - ] - }, - "seizable_assets": { - "description": "The assets that can be seized. Resets the list to this one every time it is executed", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "AssetWithLimit": { - "description": "This struct holds parameters to help with swapping a specific amount of a fee token to ASTRO.", - "type": "object", - "required": [ - "info" - ], - "properties": { - "info": { - "description": "Information about the fee token to swap", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "limit": { - "description": "The amount of tokens to swap", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "DevFundConfig": { - "type": "object", - "required": [ - "address", - "asset_info", - "share" - ], - "properties": { - "address": { - "description": "The dev fund address", - "type": "string" - }, - "asset_info": { - "description": "Asset that devs want ASTRO to be swapped to", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "share": { - "description": "The percentage of fees that go to the dev fund", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - } - }, - "additionalProperties": false - }, - "SecondReceiverParams": { - "description": "This structure describes the parameters for updating the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver", - "type": "string" - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - }, - "UpdateAddr": { - "description": "This is an enum used for setting and removing a contract address.", - "oneOf": [ - { - "description": "Sets a new contract address.", - "type": "object", - "required": [ - "set" - ], - "properties": { - "set": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "Removes a contract address.", - "type": "object", - "required": [ - "remove" - ], - "properties": { - "remove": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "UpdateDevFundConfig": { - "type": "object", - "properties": { - "set": { - "description": "If 'set' is None then dev fund config will be removed, otherwise it will be updated with the new parameters", - "anyOf": [ - { - "$ref": "#/definitions/DevFundConfig" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-maker/raw/instantiate.json b/schemas/astroport-maker/raw/instantiate.json deleted file mode 100644 index b7943f02e..000000000 --- a/schemas/astroport-maker/raw/instantiate.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure stores general parameters for the contract.", - "type": "object", - "required": [ - "astro_token", - "factory_contract", - "owner" - ], - "properties": { - "astro_token": { - "description": "The ASTRO token asset info", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "collect_cooldown": { - "description": "If set defines the period when maker collect can be called", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "default_bridge": { - "description": "Default bridge asset (Terra1 - LUNC, Terra2 - LUNA, etc.)", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "factory_contract": { - "description": "The factory contract address", - "type": "string" - }, - "governance_contract": { - "description": "The governance contract address (fee distributor for vxASTRO)", - "type": [ - "string", - "null" - ] - }, - "governance_percent": { - "description": "The percentage of fees that go to governance_contract", - "anyOf": [ - { - "$ref": "#/definitions/Uint64" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "description": "The maximum spread used when swapping fee tokens to ASTRO", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "owner": { - "description": "Address that's allowed to change contract parameters", - "type": "string" - }, - "second_receiver_params": { - "description": "The second receiver parameters of fees", - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverParams" - }, - { - "type": "null" - } - ] - }, - "staking_contract": { - "description": "The xASTRO staking contract address. If None then governance_contract must be set with 100% fee.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "SecondReceiverParams": { - "description": "This structure describes the parameters for updating the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver", - "type": "string" - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-maker/raw/migrate.json b/schemas/astroport-maker/raw/migrate.json deleted file mode 100644 index dcdb89cc4..000000000 --- a/schemas/astroport-maker/raw/migrate.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message.", - "type": "object", - "properties": { - "collect_cooldown": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "second_receiver_params": { - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverParams" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "SecondReceiverParams": { - "description": "This structure describes the parameters for updating the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver", - "type": "string" - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-maker/raw/query.json b/schemas/astroport-maker/raw/query.json deleted file mode 100644 index c1602afb3..000000000 --- a/schemas/astroport-maker/raw/query.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query functions available in the contract.", - "oneOf": [ - { - "description": "Returns information about the maker configs that contains in the [`ConfigResponse`]", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance for each asset in the specified input parameters", - "type": "object", - "required": [ - "balances" - ], - "properties": { - "balances": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "bridges" - ], - "properties": { - "bridges": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the seize config", - "type": "object", - "required": [ - "query_seize_config" - ], - "properties": { - "query_seize_config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-maker/raw/response_to_balances.json b/schemas/astroport-maker/raw/response_to_balances.json deleted file mode 100644 index 08340407b..000000000 --- a/schemas/astroport-maker/raw/response_to_balances.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalancesResponse", - "description": "A custom struct used to return multiple asset balances.", - "type": "object", - "required": [ - "balances" - ], - "properties": { - "balances": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-maker/raw/response_to_bridges.json b/schemas/astroport-maker/raw/response_to_bridges.json deleted file mode 100644 index 7d9d67ae0..000000000 --- a/schemas/astroport-maker/raw/response_to_bridges.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Tuple_of_String_and_String", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "maxItems": 2, - "minItems": 2 - } -} diff --git a/schemas/astroport-maker/raw/response_to_config.json b/schemas/astroport-maker/raw/response_to_config.json deleted file mode 100644 index 2e2bcf042..000000000 --- a/schemas/astroport-maker/raw/response_to_config.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "A custom struct that holds contract parameters and is used to retrieve them.", - "type": "object", - "required": [ - "astro_token", - "factory_contract", - "governance_percent", - "max_spread", - "owner", - "pre_upgrade_astro_amount", - "remainder_reward" - ], - "properties": { - "astro_token": { - "description": "The ASTRO token asset info", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "default_bridge": { - "description": "Default bridge (Terra1 - LUNC, Terra2 - LUNA, etc.)", - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "dev_fund_conf": { - "description": "The dev fund configuration", - "anyOf": [ - { - "$ref": "#/definitions/DevFundConfig" - }, - { - "type": "null" - } - ] - }, - "factory_contract": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "governance_contract": { - "description": "The governance contract address (fee distributor for vxASTRO stakers)", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "governance_percent": { - "description": "The percentage of fees that go to governance_contract", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "max_spread": { - "description": "The maximum spread used when swapping fee tokens to ASTRO", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "owner": { - "description": "Address that is allowed to update contract parameters", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "pre_upgrade_astro_amount": { - "description": "The amount of ASTRO tokens accrued before upgrading the Maker implementation and enabling reward distribution", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "remainder_reward": { - "description": "The remainder ASTRO tokens (accrued before the Maker is upgraded) to be distributed to xASTRO stakers", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "second_receiver_cfg": { - "description": "Parameters that describe the second receiver of fees", - "anyOf": [ - { - "$ref": "#/definitions/SecondReceiverConfig" - }, - { - "type": "null" - } - ] - }, - "staking_contract": { - "description": "The xASTRO staking contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "DevFundConfig": { - "type": "object", - "required": [ - "address", - "asset_info", - "share" - ], - "properties": { - "address": { - "description": "The dev fund address", - "type": "string" - }, - "asset_info": { - "description": "Asset that devs want ASTRO to be swapped to", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - }, - "share": { - "description": "The percentage of fees that go to the dev fund", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - } - }, - "additionalProperties": false - }, - "SecondReceiverConfig": { - "description": "This structure stores the parameters for the second receiver of fees.", - "type": "object", - "required": [ - "second_fee_receiver", - "second_receiver_cut" - ], - "properties": { - "second_fee_receiver": { - "description": "The second fee receiver contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "second_receiver_cut": { - "description": "The percentage of fees that go to the second fee receiver", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-maker/raw/response_to_query_seize_config.json b/schemas/astroport-maker/raw/response_to_query_seize_config.json deleted file mode 100644 index 874f52574..000000000 --- a/schemas/astroport-maker/raw/response_to_query_seize_config.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SeizeConfig", - "type": "object", - "required": [ - "receiver", - "seizable_assets" - ], - "properties": { - "receiver": { - "description": "The address of the contract that will receive the seized tokens", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "seizable_assets": { - "description": "The assets that can be seized", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/astroport-pair-concentrated-duality.json b/schemas/astroport-pair-concentrated-duality/astroport-pair-concentrated-duality.json deleted file mode 100644 index d990b44d3..000000000 --- a/schemas/astroport-pair-concentrated-duality/astroport-pair-concentrated-duality.json +++ /dev/null @@ -1,1807 +0,0 @@ -{ - "contract_name": "astroport-pair-concentrated-duality", - "contract_version": "4.3.3", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/DualityPairMsg" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "DualityPairMsg": { - "oneOf": [ - { - "type": "object", - "required": [ - "sync_orderbook" - ], - "properties": { - "sync_orderbook": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "update_orderbook_config" - ], - "properties": { - "update_orderbook_config": { - "$ref": "#/definitions/UpdateDualityOrderbook" - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "UpdateDualityOrderbook": { - "type": "object", - "properties": { - "avg_price_adjustment": { - "description": "Due to possible rounding issues on Duality side we have to set price tolerance, which serves as a worsening factor for the end price from PCL. Should be relatively low something like 1-10 bps.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "enable": { - "description": "Determines whether the orderbook is enabled", - "type": [ - "boolean", - "null" - ] - }, - "executor": { - "description": "The address of the orderbook sync executor", - "type": [ - "string", - "null" - ] - }, - "liquidity_percent": { - "description": "Percent of liquidity to be deployed to the orderbook", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "min_asset_0_order_size": { - "description": "Minimum order size for asset 0", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "min_asset_1_order_size": { - "description": "Minimum order size for asset 1", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "orders_number": { - "description": "Number of orders on each side of the orderbook", - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 - }, - "remove_executor": { - "description": "Determines whether the executor should be removed. If removed, then sync endpoint becomes permissionless", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": null, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query_compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulate_provide": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "simulate_withdraw": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/execute.json b/schemas/astroport-pair-concentrated-duality/raw/execute.json deleted file mode 100644 index 63b93f5e3..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/execute.json +++ /dev/null @@ -1,486 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/DualityPairMsg" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "DualityPairMsg": { - "oneOf": [ - { - "type": "object", - "required": [ - "sync_orderbook" - ], - "properties": { - "sync_orderbook": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "update_orderbook_config" - ], - "properties": { - "update_orderbook_config": { - "$ref": "#/definitions/UpdateDualityOrderbook" - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "UpdateDualityOrderbook": { - "type": "object", - "properties": { - "avg_price_adjustment": { - "description": "Due to possible rounding issues on Duality side we have to set price tolerance, which serves as a worsening factor for the end price from PCL. Should be relatively low something like 1-10 bps.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "enable": { - "description": "Determines whether the orderbook is enabled", - "type": [ - "boolean", - "null" - ] - }, - "executor": { - "description": "The address of the orderbook sync executor", - "type": [ - "string", - "null" - ] - }, - "liquidity_percent": { - "description": "Percent of liquidity to be deployed to the orderbook", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "min_asset_0_order_size": { - "description": "Minimum order size for asset 0", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "min_asset_1_order_size": { - "description": "Minimum order size for asset 1", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "orders_number": { - "description": "Number of orders on each side of the orderbook", - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 - }, - "remove_executor": { - "description": "Determines whether the executor should be removed. If removed, then sync endpoint becomes permissionless", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/instantiate.json b/schemas/astroport-pair-concentrated-duality/raw/instantiate.json deleted file mode 100644 index 00ccada94..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/instantiate.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/query.json b/schemas/astroport-pair-concentrated-duality/raw/query.json deleted file mode 100644 index b7da792ec..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/query.json +++ /dev/null @@ -1,364 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_config.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_config.json deleted file mode 100644 index 953b0adc7..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_config.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_observe.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_pair.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_pair.json deleted file mode 100644 index 16721f81d..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_pair.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_pool.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_query_compute_d.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_query_compute_d.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_query_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_share.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_simulate_provide.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_simulate_provide.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_simulate_provide.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_simulate_withdraw.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_simulate_withdraw.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_simulate_withdraw.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-duality/raw/response_to_simulation.json b/schemas/astroport-pair-concentrated-duality/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-concentrated-duality/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/astroport-pair-concentrated-sale-tax.json b/schemas/astroport-pair-concentrated-sale-tax/astroport-pair-concentrated-sale-tax.json deleted file mode 100644 index 55ff9b34c..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/astroport-pair-concentrated-sale-tax.json +++ /dev/null @@ -1,1730 +0,0 @@ -{ - "contract_name": "astroport-pair-concentrated-sale-tax", - "contract_version": "4.2.2", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a reverse swap simulation", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant", - "type": "object", - "required": [ - "compute_d" - ], - "properties": { - "compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query LP token virtual price", - "type": "object", - "required": [ - "lp_price" - ], - "properties": { - "lp_price": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false - }, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "lp_price": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulate_provide": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "simulate_withdraw": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/execute.json b/schemas/astroport-pair-concentrated-sale-tax/raw/execute.json deleted file mode 100644 index 3a6ac6f12..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/execute.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/instantiate.json b/schemas/astroport-pair-concentrated-sale-tax/raw/instantiate.json deleted file mode 100644 index 00ccada94..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/instantiate.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/migrate.json b/schemas/astroport-pair-concentrated-sale-tax/raw/migrate.json deleted file mode 100644 index 1b9dcecf9..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/migrate.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/query.json b/schemas/astroport-pair-concentrated-sale-tax/raw/query.json deleted file mode 100644 index ea222d022..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/query.json +++ /dev/null @@ -1,378 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a reverse swap simulation", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant", - "type": "object", - "required": [ - "compute_d" - ], - "properties": { - "compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query LP token virtual price", - "type": "object", - "required": [ - "lp_price" - ], - "properties": { - "lp_price": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_compute_d.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_compute_d.json deleted file mode 100644 index def41169f..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_config.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_config.json deleted file mode 100644 index 953b0adc7..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_config.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_lp_price.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_lp_price.json deleted file mode 100644 index def41169f..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_lp_price.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_observe.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_pair.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_pair.json deleted file mode 100644 index 16721f81d..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_pair.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_pool.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_share.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulate_provide.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulate_provide.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulate_provide.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulate_withdraw.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulate_withdraw.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulate_withdraw.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulation.json b/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-concentrated-sale-tax/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/astroport-pair-concentrated.json b/schemas/astroport-pair-concentrated/astroport-pair-concentrated.json deleted file mode 100644 index 78f9d442e..000000000 --- a/schemas/astroport-pair-concentrated/astroport-pair-concentrated.json +++ /dev/null @@ -1,1730 +0,0 @@ -{ - "contract_name": "astroport-pair-concentrated", - "contract_version": "4.2.2", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a reverse swap simulation", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant", - "type": "object", - "required": [ - "compute_d" - ], - "properties": { - "compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query LP token virtual price", - "type": "object", - "required": [ - "lp_price" - ], - "properties": { - "lp_price": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false - }, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "lp_price": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulate_provide": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "simulate_withdraw": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/execute.json b/schemas/astroport-pair-concentrated/raw/execute.json deleted file mode 100644 index 3a6ac6f12..000000000 --- a/schemas/astroport-pair-concentrated/raw/execute.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/instantiate.json b/schemas/astroport-pair-concentrated/raw/instantiate.json deleted file mode 100644 index 00ccada94..000000000 --- a/schemas/astroport-pair-concentrated/raw/instantiate.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/migrate.json b/schemas/astroport-pair-concentrated/raw/migrate.json deleted file mode 100644 index 1b9dcecf9..000000000 --- a/schemas/astroport-pair-concentrated/raw/migrate.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false -} diff --git a/schemas/astroport-pair-concentrated/raw/query.json b/schemas/astroport-pair-concentrated/raw/query.json deleted file mode 100644 index ea222d022..000000000 --- a/schemas/astroport-pair-concentrated/raw/query.json +++ /dev/null @@ -1,378 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a reverse swap simulation", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant", - "type": "object", - "required": [ - "compute_d" - ], - "properties": { - "compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query LP token virtual price", - "type": "object", - "required": [ - "lp_price" - ], - "properties": { - "lp_price": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-concentrated/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_compute_d.json b/schemas/astroport-pair-concentrated/raw/response_to_compute_d.json deleted file mode 100644 index def41169f..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_config.json b/schemas/astroport-pair-concentrated/raw/response_to_config.json deleted file mode 100644 index 953b0adc7..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_config.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-concentrated/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_lp_price.json b/schemas/astroport-pair-concentrated/raw/response_to_lp_price.json deleted file mode 100644 index def41169f..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_lp_price.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal256", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal256(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 115792089237316195423570985008687907853269984665640564039457.584007913129639935 (which is (2^256 - 1) / 10^18)", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_observe.json b/schemas/astroport-pair-concentrated/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_pair.json b/schemas/astroport-pair-concentrated/raw/response_to_pair.json deleted file mode 100644 index 16721f81d..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_pair.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_pool.json b/schemas/astroport-pair-concentrated/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-concentrated/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_share.json b/schemas/astroport-pair-concentrated/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_simulate_provide.json b/schemas/astroport-pair-concentrated/raw/response_to_simulate_provide.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_simulate_provide.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_simulate_withdraw.json b/schemas/astroport-pair-concentrated/raw/response_to_simulate_withdraw.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_simulate_withdraw.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-concentrated/raw/response_to_simulation.json b/schemas/astroport-pair-concentrated/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-concentrated/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/astroport-pair-converter.json b/schemas/astroport-pair-converter/astroport-pair-converter.json deleted file mode 100644 index 9723d2c10..000000000 --- a/schemas/astroport-pair-converter/astroport-pair-converter.json +++ /dev/null @@ -1,1428 +0,0 @@ -{ - "contract_name": "astroport-pair-converter", - "contract_version": "1.1.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Generator contract", - "type": [ - "boolean", - "null" - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false - }, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query_compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-converter/raw/execute.json b/schemas/astroport-pair-converter/raw/execute.json deleted file mode 100644 index 179a53617..000000000 --- a/schemas/astroport-pair-converter/raw/execute.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Generator contract", - "type": [ - "boolean", - "null" - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/instantiate.json b/schemas/astroport-pair-converter/raw/instantiate.json deleted file mode 100644 index 9d812253f..000000000 --- a/schemas/astroport-pair-converter/raw/instantiate.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/migrate.json b/schemas/astroport-pair-converter/raw/migrate.json deleted file mode 100644 index 1b9dcecf9..000000000 --- a/schemas/astroport-pair-converter/raw/migrate.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false -} diff --git a/schemas/astroport-pair-converter/raw/query.json b/schemas/astroport-pair-converter/raw/query.json deleted file mode 100644 index c63191060..000000000 --- a/schemas/astroport-pair-converter/raw/query.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-converter/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_config.json b/schemas/astroport-pair-converter/raw/response_to_config.json deleted file mode 100644 index ce805e9c4..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_config.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-converter/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_observe.json b/schemas/astroport-pair-converter/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_pair.json b/schemas/astroport-pair-converter/raw/response_to_pair.json deleted file mode 100644 index 837f3f8dc..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_pair.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_pool.json b/schemas/astroport-pair-converter/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_query_compute_d.json b/schemas/astroport-pair-converter/raw/response_to_query_compute_d.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_query_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-converter/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-converter/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_share.json b/schemas/astroport-pair-converter/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-converter/raw/response_to_simulation.json b/schemas/astroport-pair-converter/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-converter/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/astroport-pair-stable.json b/schemas/astroport-pair-stable/astroport-pair-stable.json deleted file mode 100644 index aad48a65e..000000000 --- a/schemas/astroport-pair-stable/astroport-pair-stable.json +++ /dev/null @@ -1,1710 +0,0 @@ -{ - "contract_name": "astroport-pair-stable", - "contract_version": "4.2.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false - }, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query_compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulate_provide": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "simulate_withdraw": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-stable/raw/execute.json b/schemas/astroport-pair-stable/raw/execute.json deleted file mode 100644 index 3a6ac6f12..000000000 --- a/schemas/astroport-pair-stable/raw/execute.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/instantiate.json b/schemas/astroport-pair-stable/raw/instantiate.json deleted file mode 100644 index 00ccada94..000000000 --- a/schemas/astroport-pair-stable/raw/instantiate.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-stable/raw/migrate.json b/schemas/astroport-pair-stable/raw/migrate.json deleted file mode 100644 index 1b9dcecf9..000000000 --- a/schemas/astroport-pair-stable/raw/migrate.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes a migration message. We currently take no arguments for migrations.", - "type": "object", - "additionalProperties": false -} diff --git a/schemas/astroport-pair-stable/raw/query.json b/schemas/astroport-pair-stable/raw/query.json deleted file mode 100644 index b7da792ec..000000000 --- a/schemas/astroport-pair-stable/raw/query.json +++ /dev/null @@ -1,364 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-stable/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_config.json b/schemas/astroport-pair-stable/raw/response_to_config.json deleted file mode 100644 index 953b0adc7..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_config.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-stable/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_observe.json b/schemas/astroport-pair-stable/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_pair.json b/schemas/astroport-pair-stable/raw/response_to_pair.json deleted file mode 100644 index 16721f81d..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_pair.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_pool.json b/schemas/astroport-pair-stable/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_query_compute_d.json b/schemas/astroport-pair-stable/raw/response_to_query_compute_d.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_query_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-stable/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-stable/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_share.json b/schemas/astroport-pair-stable/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_simulate_provide.json b/schemas/astroport-pair-stable/raw/response_to_simulate_provide.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_simulate_provide.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-stable/raw/response_to_simulate_withdraw.json b/schemas/astroport-pair-stable/raw/response_to_simulate_withdraw.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_simulate_withdraw.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-stable/raw/response_to_simulation.json b/schemas/astroport-pair-stable/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-stable/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/astroport-pair-xastro.json b/schemas/astroport-pair-xastro/astroport-pair-xastro.json deleted file mode 100644 index f025b92ac..000000000 --- a/schemas/astroport-pair-xastro/astroport-pair-xastro.json +++ /dev/null @@ -1,1704 +0,0 @@ -{ - "contract_name": "astroport-pair-xastro", - "contract_version": "1.0.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": null, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query_compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulate_provide": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "simulate_withdraw": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/execute.json b/schemas/astroport-pair-xastro/raw/execute.json deleted file mode 100644 index 3a6ac6f12..000000000 --- a/schemas/astroport-pair-xastro/raw/execute.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/instantiate.json b/schemas/astroport-pair-xastro/raw/instantiate.json deleted file mode 100644 index 00ccada94..000000000 --- a/schemas/astroport-pair-xastro/raw/instantiate.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/query.json b/schemas/astroport-pair-xastro/raw/query.json deleted file mode 100644 index b7da792ec..000000000 --- a/schemas/astroport-pair-xastro/raw/query.json +++ /dev/null @@ -1,364 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-xastro/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_config.json b/schemas/astroport-pair-xastro/raw/response_to_config.json deleted file mode 100644 index 953b0adc7..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_config.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-xastro/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_observe.json b/schemas/astroport-pair-xastro/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_pair.json b/schemas/astroport-pair-xastro/raw/response_to_pair.json deleted file mode 100644 index 16721f81d..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_pair.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_pool.json b/schemas/astroport-pair-xastro/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_query_compute_d.json b/schemas/astroport-pair-xastro/raw/response_to_query_compute_d.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_query_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-xastro/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_share.json b/schemas/astroport-pair-xastro/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_simulate_provide.json b/schemas/astroport-pair-xastro/raw/response_to_simulate_provide.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_simulate_provide.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_simulate_withdraw.json b/schemas/astroport-pair-xastro/raw/response_to_simulate_withdraw.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_simulate_withdraw.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xastro/raw/response_to_simulation.json b/schemas/astroport-pair-xastro/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-xastro/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/astroport-pair-xyk-sale-tax.json b/schemas/astroport-pair-xyk-sale-tax/astroport-pair-xyk-sale-tax.json deleted file mode 100644 index a2583bb72..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/astroport-pair-xyk-sale-tax.json +++ /dev/null @@ -1,1763 +0,0 @@ -{ - "contract_name": "astroport-pair-xyk-sale-tax", - "contract_version": "2.2.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "Message used when migrating the contract from the standard XYK pair.", - "type": "object", - "required": [ - "tax_config_admin", - "tax_configs" - ], - "properties": { - "tax_config_admin": { - "description": "The address that is allowed to updated the tax configs.", - "type": "string" - }, - "tax_configs": { - "description": "The configs of the trade taxes for the pair.", - "allOf": [ - { - "$ref": "#/definitions/TaxConfigs_for_String" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "TaxConfig_for_String": { - "type": "object", - "required": [ - "tax_rate", - "tax_recipient" - ], - "properties": { - "tax_rate": { - "description": "The tax rate to apply to token sales of `tax_denom`.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "tax_recipient": { - "description": "The address to send the tax to", - "type": "string" - } - }, - "additionalProperties": false - }, - "TaxConfigs_for_String": { - "description": "A map of tax configs, keyed by the denom of the asset to tax sales of. E.g. in the pair APOLLO-USDC, the can have one tax rate and recipient when swapping APOLLO for USDC, and another when swapping USDC for APOLLO.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/TaxConfig_for_String" - } - } - } - }, - "sudo": null, - "responses": { - "asset_balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "cumulative_prices": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "observe": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } - }, - "pair": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } - }, - "pool": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "query_compute_d": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "reverse_simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "share": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulate_provide": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "simulate_withdraw": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "simulation": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/execute.json b/schemas/astroport-pair-xyk-sale-tax/raw/execute.json deleted file mode 100644 index 3a6ac6f12..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/execute.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Receives a message of type [`Cw20ReceiveMsg`]", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "ProvideLiquidity allows someone to provide liquidity in the pool", - "type": "object", - "required": [ - "provide_liquidity" - ], - "properties": { - "provide_liquidity": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "description": "The assets available in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "auto_stake": { - "description": "Determines whether the LP tokens minted for the user is auto_staked in the Incentives contract", - "type": [ - "boolean", - "null" - ] - }, - "min_lp_to_receive": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "receiver": { - "description": "The receiver of LP tokens", - "type": [ - "string", - "null" - ] - }, - "slippage_tolerance": { - "description": "The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "WithdrawLiquidity allows someone to withdraw liquidity from the pool", - "type": "object", - "required": [ - "withdraw_liquidity" - ], - "properties": { - "withdraw_liquidity": { - "type": "object", - "properties": { - "assets": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "min_assets_to_receive": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Asset" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Swap performs a swap in the pool", - "type": "object", - "required": [ - "swap" - ], - "properties": { - "swap": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "belief_price": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_spread": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - }, - "to": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the pair configuration", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "params" - ], - "properties": { - "params": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "ProposeNewOwner creates a proposal to change contract ownership. The validity period for the proposal is set in the `expires_in` variable.", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The date after which this proposal expires", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "Newly proposed contract owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "DropOwnershipProposal removes the existing offer to change contract ownership.", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Used to claim contract ownership.", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom execute endpoints for extended pool implementations", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/instantiate.json b/schemas/astroport-pair-xyk-sale-tax/raw/instantiate.json deleted file mode 100644 index 00ccada94..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/instantiate.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "asset_infos", - "factory_addr", - "pair_type", - "token_code_id" - ], - "properties": { - "asset_infos": { - "description": "Information about assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "factory_addr": { - "description": "The factory contract address", - "type": "string" - }, - "init_params": { - "description": "Optional binary serialised parameters for custom pool types", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "pair_type": { - "description": "The pair type", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - }, - "token_code_id": { - "description": "The token contract code ID used for the tokens in the pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/migrate.json b/schemas/astroport-pair-xyk-sale-tax/raw/migrate.json deleted file mode 100644 index 10400ed19..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/migrate.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "Message used when migrating the contract from the standard XYK pair.", - "type": "object", - "required": [ - "tax_config_admin", - "tax_configs" - ], - "properties": { - "tax_config_admin": { - "description": "The address that is allowed to updated the tax configs.", - "type": "string" - }, - "tax_configs": { - "description": "The configs of the trade taxes for the pair.", - "allOf": [ - { - "$ref": "#/definitions/TaxConfigs_for_String" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "TaxConfig_for_String": { - "type": "object", - "required": [ - "tax_rate", - "tax_recipient" - ], - "properties": { - "tax_rate": { - "description": "The tax rate to apply to token sales of `tax_denom`.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "tax_recipient": { - "description": "The address to send the tax to", - "type": "string" - } - }, - "additionalProperties": false - }, - "TaxConfigs_for_String": { - "description": "A map of tax configs, keyed by the denom of the asset to tax sales of. E.g. in the pair APOLLO-USDC, the can have one tax rate and recipient when swapping APOLLO for USDC, and another when swapping USDC for APOLLO.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/TaxConfig_for_String" - } - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/query.json b/schemas/astroport-pair-xyk-sale-tax/raw/query.json deleted file mode 100644 index b7da792ec..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/query.json +++ /dev/null @@ -1,364 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns information about a pair in an object of type [`super::asset::PairInfo`].", - "type": "object", - "required": [ - "pair" - ], - "properties": { - "pair": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a pool in an object of type [`PoolResponse`].", - "type": "object", - "required": [ - "pool" - ], - "properties": { - "pool": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns contract configuration settings in a custom [`ConfigResponse`] structure.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the share of the pool in a vector that contains objects of type [`Asset`].", - "type": "object", - "required": [ - "share" - ], - "properties": { - "share": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about a swap simulation in a [`SimulationResponse`] object.", - "type": "object", - "required": [ - "simulation" - ], - "properties": { - "simulation": { - "type": "object", - "required": [ - "offer_asset" - ], - "properties": { - "ask_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - }, - "offer_asset": { - "$ref": "#/definitions/Asset" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about cumulative prices in a [`ReverseSimulationResponse`] object.", - "type": "object", - "required": [ - "reverse_simulation" - ], - "properties": { - "reverse_simulation": { - "type": "object", - "required": [ - "ask_asset" - ], - "properties": { - "ask_asset": { - "$ref": "#/definitions/Asset" - }, - "offer_asset_info": { - "anyOf": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about the cumulative prices in a [`CumulativePricesResponse`] object", - "type": "object", - "required": [ - "cumulative_prices" - ], - "properties": { - "cumulative_prices": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns current D invariant in as a [`u128`] value", - "type": "object", - "required": [ - "query_compute_d" - ], - "properties": { - "query_compute_d": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the balance of the specified asset that was in the pool just preceeding the moment of the specified block height creation.", - "type": "object", - "required": [ - "asset_balance_at" - ], - "properties": { - "asset_balance_at": { - "type": "object", - "required": [ - "asset_info", - "block_height" - ], - "properties": { - "asset_info": { - "$ref": "#/definitions/AssetInfo" - }, - "block_height": { - "$ref": "#/definitions/Uint64" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query price from observations", - "type": "object", - "required": [ - "observe" - ], - "properties": { - "observe": { - "type": "object", - "required": [ - "seconds_ago" - ], - "properties": { - "seconds_ago": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of assets received for the given amount of LP tokens", - "type": "object", - "required": [ - "simulate_withdraw" - ], - "properties": { - "simulate_withdraw": { - "type": "object", - "required": [ - "lp_amount" - ], - "properties": { - "lp_amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns an estimation of shares received for the given amount of assets", - "type": "object", - "required": [ - "simulate_provide" - ], - "properties": { - "simulate_provide": { - "type": "object", - "required": [ - "assets" - ], - "properties": { - "assets": { - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "slippage_tolerance": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_asset_balance_at.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_asset_balance_at.json deleted file mode 100644 index 2eaf6e96f..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_asset_balance_at.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_Uint128", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ], - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_config.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_config.json deleted file mode 100644 index 953b0adc7..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_config.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This struct is used to return a query result with the general contract configuration.", - "type": "object", - "required": [ - "block_time_last", - "factory_addr", - "owner" - ], - "properties": { - "block_time_last": { - "description": "Last timestamp when the cumulative prices in the pool were updated", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "factory_addr": { - "description": "The factory contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "owner": { - "description": "The contract owner", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "params": { - "description": "The pool's parameters", - "anyOf": [ - { - "$ref": "#/definitions/Binary" - }, - { - "type": "null" - } - ] - }, - "tracker_addr": { - "description": "Tracker contract address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_cumulative_prices.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_cumulative_prices.json deleted file mode 100644 index 695a3121f..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_cumulative_prices.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CumulativePricesResponse", - "description": "This structure is used to return a cumulative prices query response.", - "type": "object", - "required": [ - "assets", - "cumulative_prices", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool to query", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "cumulative_prices": { - "description": "The vector contains cumulative prices for each pair of assets in the pool", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/AssetInfo" - }, - { - "$ref": "#/definitions/Uint128" - } - ], - "maxItems": 3, - "minItems": 3 - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_observe.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_observe.json deleted file mode 100644 index a8c389cb0..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_observe.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OracleObservation", - "type": "object", - "required": [ - "price", - "timestamp" - ], - "properties": { - "price": { - "$ref": "#/definitions/Decimal" - }, - "timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_pair.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_pair.json deleted file mode 100644 index 16721f81d..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_pair.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PairInfo", - "description": "This structure stores the main parameters for an Astroport pair", - "type": "object", - "required": [ - "asset_infos", - "contract_addr", - "liquidity_token", - "pair_type" - ], - "properties": { - "asset_infos": { - "description": "Asset information for the assets in the pool", - "type": "array", - "items": { - "$ref": "#/definitions/AssetInfo" - } - }, - "contract_addr": { - "description": "Pair contract address", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "liquidity_token": { - "description": "Pair LP token denom", - "type": "string" - }, - "pair_type": { - "description": "The pool type (xyk, stableswap etc) available in [`PairType`]", - "allOf": [ - { - "$ref": "#/definitions/PairType" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "PairType": { - "description": "This enum describes available pair types. ## Available pool types ``` # use astroport::factory::PairType::{Custom, Stable, Xyk}; Xyk {}; Stable {}; Custom(String::from(\"Custom\")); ```", - "oneOf": [ - { - "description": "XYK pair type", - "type": "object", - "required": [ - "xyk" - ], - "properties": { - "xyk": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Stable pair type", - "type": "object", - "required": [ - "stable" - ], - "properties": { - "stable": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Custom pair type", - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "type": "string" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_pool.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_pool.json deleted file mode 100644 index 693cad0e0..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_pool.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PoolResponse", - "description": "This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool.", - "type": "object", - "required": [ - "assets", - "total_share" - ], - "properties": { - "assets": { - "description": "The assets in the pool together with asset amounts", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - } - }, - "total_share": { - "description": "The total amount of LP tokens currently issued", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_query_compute_d.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_query_compute_d.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_query_compute_d.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_reverse_simulation.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_reverse_simulation.json deleted file mode 100644 index ca711e182..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_reverse_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ReverseSimulationResponse", - "description": "This structure holds the parameters that are returned from a reverse swap simulation response.", - "type": "object", - "required": [ - "commission_amount", - "offer_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "offer_amount": { - "description": "The amount of offer assets returned by the reverse swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_share.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_share.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_share.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulate_provide.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulate_provide.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulate_provide.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulate_withdraw.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulate_withdraw.json deleted file mode 100644 index 8285d4916..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulate_withdraw.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_Asset", - "type": "array", - "items": { - "$ref": "#/definitions/Asset" - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Asset": { - "description": "This enum describes a Terra asset (native or CW20).", - "type": "object", - "required": [ - "amount", - "info" - ], - "properties": { - "amount": { - "description": "A token amount", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "info": { - "description": "Information about an asset stored in a [`AssetInfo`] struct", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulation.json b/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulation.json deleted file mode 100644 index 4bc828d4a..000000000 --- a/schemas/astroport-pair-xyk-sale-tax/raw/response_to_simulation.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "SimulationResponse", - "description": "This structure holds the parameters that are returned from a swap simulation response", - "type": "object", - "required": [ - "commission_amount", - "return_amount", - "spread_amount" - ], - "properties": { - "commission_amount": { - "description": "The amount of fees charged by the transaction", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "return_amount": { - "description": "The amount of ask assets returned by the swap", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "spread_amount": { - "description": "The spread used in the swap operation", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-staking/astroport-staking.json b/schemas/astroport-staking/astroport-staking.json deleted file mode 100644 index 96a2cbbcb..000000000 --- a/schemas/astroport-staking/astroport-staking.json +++ /dev/null @@ -1,319 +0,0 @@ -{ - "contract_name": "astroport-staking", - "contract_version": "2.3.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "deposit_token_denom", - "token_factory_addr", - "tracking_admin", - "tracking_code_id" - ], - "properties": { - "deposit_token_denom": { - "description": "The ASTRO token contract address", - "type": "string" - }, - "token_factory_addr": { - "description": "Token factory module address. Contract creator must ensure that the address is exact token factory module address.", - "type": "string" - }, - "tracking_admin": { - "description": "Tracking contract admin", - "type": "string" - }, - "tracking_code_id": { - "description": "The Code ID of contract used to track the TokenFactory token balances", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Deposits ASTRO in exchange for xASTRO The receiver is optional. If not set, the sender will receive the xASTRO.", - "type": "object", - "required": [ - "enter" - ], - "properties": { - "enter": { - "type": "object", - "properties": { - "receiver": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Deposits ASTRO in exchange for xASTRO and passes **all resulting xASTRO** to defined contract along with an executable message.", - "type": "object", - "required": [ - "enter_with_hook" - ], - "properties": { - "enter_with_hook": { - "type": "object", - "required": [ - "contract_address", - "msg" - ], - "properties": { - "contract_address": { - "type": "string" - }, - "msg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Burns xASTRO in exchange for ASTRO. The receiver is optional. If not set, the sender will receive the ASTRO.", - "type": "object", - "required": [ - "leave" - ], - "properties": { - "leave": { - "type": "object", - "properties": { - "receiver": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Config returns the contract configuration specified in a custom [`Config`] structure", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns xASTRO total supply. Duplicates TotalSupplyAt { timestamp: None } logic but kept for backward compatibility.", - "type": "object", - "required": [ - "total_shares" - ], - "properties": { - "total_shares": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns total ASTRO staked in the contract", - "type": "object", - "required": [ - "total_deposit" - ], - "properties": { - "total_deposit": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "tracker_config" - ], - "properties": { - "tracker_config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "BalanceAt returns xASTRO balance of the given address at at the given timestamp. Returns current balance if timestamp unset.", - "type": "object", - "required": [ - "balance_at" - ], - "properties": { - "balance_at": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - }, - "timestamp": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "TotalSupplyAt returns xASTRO total token supply at the given timestamp. Returns current total supply if timestamp unset.", - "type": "object", - "required": [ - "total_supply_at" - ], - "properties": { - "total_supply_at": { - "type": "object", - "properties": { - "timestamp": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "migrate": null, - "sudo": null, - "responses": { - "balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Config", - "description": "This structure stores the main parameters for the staking contract.", - "type": "object", - "required": [ - "astro_denom", - "xastro_denom" - ], - "properties": { - "astro_denom": { - "description": "The ASTRO token denom", - "type": "string" - }, - "xastro_denom": { - "description": "The xASTRO token denom", - "type": "string" - } - }, - "additionalProperties": false - }, - "total_deposit": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "total_shares": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "total_supply_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "tracker_config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "TrackerData", - "description": "This structure stores the tracking contract data.", - "type": "object", - "required": [ - "admin", - "code_id", - "token_factory_addr", - "tracker_addr" - ], - "properties": { - "admin": { - "description": "Tracking contract admin", - "type": "string" - }, - "code_id": { - "description": "Tracking contract code id", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "token_factory_addr": { - "description": "Token factory module address", - "type": "string" - }, - "tracker_addr": { - "description": "Tracker contract address", - "type": "string" - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-staking/raw/execute.json b/schemas/astroport-staking/raw/execute.json deleted file mode 100644 index 670519b62..000000000 --- a/schemas/astroport-staking/raw/execute.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Deposits ASTRO in exchange for xASTRO The receiver is optional. If not set, the sender will receive the xASTRO.", - "type": "object", - "required": [ - "enter" - ], - "properties": { - "enter": { - "type": "object", - "properties": { - "receiver": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Deposits ASTRO in exchange for xASTRO and passes **all resulting xASTRO** to defined contract along with an executable message.", - "type": "object", - "required": [ - "enter_with_hook" - ], - "properties": { - "enter_with_hook": { - "type": "object", - "required": [ - "contract_address", - "msg" - ], - "properties": { - "contract_address": { - "type": "string" - }, - "msg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Burns xASTRO in exchange for ASTRO. The receiver is optional. If not set, the sender will receive the ASTRO.", - "type": "object", - "required": [ - "leave" - ], - "properties": { - "leave": { - "type": "object", - "properties": { - "receiver": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-staking/raw/instantiate.json b/schemas/astroport-staking/raw/instantiate.json deleted file mode 100644 index d97ea0a95..000000000 --- a/schemas/astroport-staking/raw/instantiate.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "deposit_token_denom", - "token_factory_addr", - "tracking_admin", - "tracking_code_id" - ], - "properties": { - "deposit_token_denom": { - "description": "The ASTRO token contract address", - "type": "string" - }, - "token_factory_addr": { - "description": "Token factory module address. Contract creator must ensure that the address is exact token factory module address.", - "type": "string" - }, - "tracking_admin": { - "description": "Tracking contract admin", - "type": "string" - }, - "tracking_code_id": { - "description": "The Code ID of contract used to track the TokenFactory token balances", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false -} diff --git a/schemas/astroport-staking/raw/query.json b/schemas/astroport-staking/raw/query.json deleted file mode 100644 index 0d85cb022..000000000 --- a/schemas/astroport-staking/raw/query.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Config returns the contract configuration specified in a custom [`Config`] structure", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns xASTRO total supply. Duplicates TotalSupplyAt { timestamp: None } logic but kept for backward compatibility.", - "type": "object", - "required": [ - "total_shares" - ], - "properties": { - "total_shares": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns total ASTRO staked in the contract", - "type": "object", - "required": [ - "total_deposit" - ], - "properties": { - "total_deposit": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "tracker_config" - ], - "properties": { - "tracker_config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "BalanceAt returns xASTRO balance of the given address at at the given timestamp. Returns current balance if timestamp unset.", - "type": "object", - "required": [ - "balance_at" - ], - "properties": { - "balance_at": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - }, - "timestamp": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "TotalSupplyAt returns xASTRO total token supply at the given timestamp. Returns current total supply if timestamp unset.", - "type": "object", - "required": [ - "total_supply_at" - ], - "properties": { - "total_supply_at": { - "type": "object", - "properties": { - "timestamp": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] -} diff --git a/schemas/astroport-staking/raw/response_to_balance_at.json b/schemas/astroport-staking/raw/response_to_balance_at.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-staking/raw/response_to_balance_at.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-staking/raw/response_to_config.json b/schemas/astroport-staking/raw/response_to_config.json deleted file mode 100644 index 26684cfb5..000000000 --- a/schemas/astroport-staking/raw/response_to_config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Config", - "description": "This structure stores the main parameters for the staking contract.", - "type": "object", - "required": [ - "astro_denom", - "xastro_denom" - ], - "properties": { - "astro_denom": { - "description": "The ASTRO token denom", - "type": "string" - }, - "xastro_denom": { - "description": "The xASTRO token denom", - "type": "string" - } - }, - "additionalProperties": false -} diff --git a/schemas/astroport-staking/raw/response_to_total_deposit.json b/schemas/astroport-staking/raw/response_to_total_deposit.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-staking/raw/response_to_total_deposit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-staking/raw/response_to_total_shares.json b/schemas/astroport-staking/raw/response_to_total_shares.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-staking/raw/response_to_total_shares.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-staking/raw/response_to_total_supply_at.json b/schemas/astroport-staking/raw/response_to_total_supply_at.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-staking/raw/response_to_total_supply_at.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-staking/raw/response_to_tracker_config.json b/schemas/astroport-staking/raw/response_to_tracker_config.json deleted file mode 100644 index 0824f5cbf..000000000 --- a/schemas/astroport-staking/raw/response_to_tracker_config.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "TrackerData", - "description": "This structure stores the tracking contract data.", - "type": "object", - "required": [ - "admin", - "code_id", - "token_factory_addr", - "tracker_addr" - ], - "properties": { - "admin": { - "description": "Tracking contract admin", - "type": "string" - }, - "code_id": { - "description": "Tracking contract code id", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "token_factory_addr": { - "description": "Token factory module address", - "type": "string" - }, - "tracker_addr": { - "description": "Tracker contract address", - "type": "string" - } - }, - "additionalProperties": false -} diff --git a/schemas/astroport-vesting/astroport-vesting.json b/schemas/astroport-vesting/astroport-vesting.json deleted file mode 100644 index 14372c857..000000000 --- a/schemas/astroport-vesting/astroport-vesting.json +++ /dev/null @@ -1,868 +0,0 @@ -{ - "contract_name": "astroport-vesting", - "contract_version": "1.4.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "owner", - "vesting_token" - ], - "properties": { - "owner": { - "description": "Address allowed to change contract parameters", - "type": "string" - }, - "vesting_token": { - "description": "[`AssetInfo`] of the token that's being vested", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Claim claims vested tokens and sends them to a recipient", - "type": "object", - "required": [ - "claim" - ], - "properties": { - "claim": { - "type": "object", - "properties": { - "amount": { - "description": "The amount of tokens to claim", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "recipient": { - "description": "The address that receives the vested tokens", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "RegisterVestingAccounts registers vesting targets/accounts", - "type": "object", - "required": [ - "register_vesting_accounts" - ], - "properties": { - "register_vesting_accounts": { - "type": "object", - "required": [ - "vesting_accounts" - ], - "properties": { - "vesting_accounts": { - "type": "array", - "items": { - "$ref": "#/definitions/VestingAccount" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Withdraws from current active schedule. Setups a new schedule with the remaining amount.", - "type": "object", - "required": [ - "withdraw_from_active_schedule" - ], - "properties": { - "withdraw_from_active_schedule": { - "type": "object", - "required": [ - "account", - "withdraw_amount" - ], - "properties": { - "account": { - "description": "The account from which tokens will be withdrawn", - "type": "string" - }, - "recipient": { - "description": "The address that receives the vested tokens", - "type": [ - "string", - "null" - ] - }, - "withdraw_amount": { - "description": "The amount of tokens to withdraw", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Creates a request to change contract ownership ## Executor Only the current owner can execute this", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The validity period of the offer to change the owner", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "The newly proposed owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes a request to change contract ownership ## Executor Only the current owner can execute this", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Claims contract ownership ## Executor Only the newly proposed owner can execute this", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "VestingAccount": { - "description": "This structure stores vesting information for a specific address that is getting tokens.", - "type": "object", - "required": [ - "address", - "schedules" - ], - "properties": { - "address": { - "description": "The address that is getting tokens", - "type": "string" - }, - "schedules": { - "description": "The vesting schedules targeted at the `address`", - "type": "array", - "items": { - "$ref": "#/definitions/VestingSchedule" - } - } - }, - "additionalProperties": false - }, - "VestingSchedule": { - "description": "This structure stores parameters for a specific vesting schedule", - "type": "object", - "required": [ - "start_point" - ], - "properties": { - "end_point": { - "description": "The end point for the vesting schedule", - "anyOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - }, - { - "type": "null" - } - ] - }, - "start_point": { - "description": "The start date for the vesting schedule", - "allOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - } - ] - } - }, - "additionalProperties": false - }, - "VestingSchedulePoint": { - "description": "This structure stores the parameters used to create a vesting schedule.", - "type": "object", - "required": [ - "amount", - "time" - ], - "properties": { - "amount": { - "description": "The amount of tokens being vested", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "time": { - "description": "The start time for the vesting schedule", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns the configuration for the contract using a [`ConfigResponse`] object.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about an address vesting tokens using a [`VestingAccountResponse`] object.", - "type": "object", - "required": [ - "vesting_account" - ], - "properties": { - "vesting_account": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object.", - "type": "object", - "required": [ - "vesting_accounts" - ], - "properties": { - "vesting_accounts": { - "type": "object", - "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "order_by": { - "anyOf": [ - { - "$ref": "#/definitions/OrderBy" - }, - { - "type": "null" - } - ] - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the total unvested amount of tokens for a specific address.", - "type": "object", - "required": [ - "available_amount" - ], - "properties": { - "available_amount": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Timestamp returns the current timestamp", - "type": "object", - "required": [ - "timestamp" - ], - "properties": { - "timestamp": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "OrderBy": { - "description": "This enum describes the types of sorting that can be applied to some piece of data", - "type": "string", - "enum": [ - "asc", - "desc" - ] - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes migration message.", - "type": "object", - "required": [ - "converter_contract" - ], - "properties": { - "converter_contract": { - "description": "Special migration message needed during the Hub move. Cw admin must be very cautious supplying correct converter contract.", - "type": "string" - } - }, - "additionalProperties": false - }, - "sudo": null, - "responses": { - "available_amount": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This structure describes a custom struct used to return the contract configuration.", - "type": "object", - "required": [ - "owner", - "vesting_token" - ], - "properties": { - "owner": { - "description": "Address allowed to set contract parameters", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "vesting_token": { - "description": "[`AssetInfo`] of the token being vested", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } - }, - "timestamp": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "uint64", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "vesting_account": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "VestingAccountResponse", - "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", - "type": "object", - "required": [ - "address", - "info" - ], - "properties": { - "address": { - "description": "The address that's vesting tokens", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "info": { - "description": "Vesting information", - "allOf": [ - { - "$ref": "#/definitions/VestingInfo" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "VestingInfo": { - "description": "This structure stores parameters for a batch of vesting schedules.", - "type": "object", - "required": [ - "released_amount", - "schedules" - ], - "properties": { - "released_amount": { - "description": "The total amount of ASTRO already claimed", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "schedules": { - "description": "The vesting schedules", - "type": "array", - "items": { - "$ref": "#/definitions/VestingSchedule" - } - } - }, - "additionalProperties": false - }, - "VestingSchedule": { - "description": "This structure stores parameters for a specific vesting schedule", - "type": "object", - "required": [ - "start_point" - ], - "properties": { - "end_point": { - "description": "The end point for the vesting schedule", - "anyOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - }, - { - "type": "null" - } - ] - }, - "start_point": { - "description": "The start date for the vesting schedule", - "allOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - } - ] - } - }, - "additionalProperties": false - }, - "VestingSchedulePoint": { - "description": "This structure stores the parameters used to create a vesting schedule.", - "type": "object", - "required": [ - "amount", - "time" - ], - "properties": { - "amount": { - "description": "The amount of tokens being vested", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "time": { - "description": "The start time for the vesting schedule", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } - }, - "vesting_accounts": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "VestingAccountsResponse", - "description": "This structure describes a custom struct used to return vesting data for multiple vesting targets.", - "type": "object", - "required": [ - "vesting_accounts" - ], - "properties": { - "vesting_accounts": { - "description": "A list of accounts that are vesting tokens", - "type": "array", - "items": { - "$ref": "#/definitions/VestingAccountResponse" - } - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "VestingAccountResponse": { - "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", - "type": "object", - "required": [ - "address", - "info" - ], - "properties": { - "address": { - "description": "The address that's vesting tokens", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "info": { - "description": "Vesting information", - "allOf": [ - { - "$ref": "#/definitions/VestingInfo" - } - ] - } - }, - "additionalProperties": false - }, - "VestingInfo": { - "description": "This structure stores parameters for a batch of vesting schedules.", - "type": "object", - "required": [ - "released_amount", - "schedules" - ], - "properties": { - "released_amount": { - "description": "The total amount of ASTRO already claimed", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "schedules": { - "description": "The vesting schedules", - "type": "array", - "items": { - "$ref": "#/definitions/VestingSchedule" - } - } - }, - "additionalProperties": false - }, - "VestingSchedule": { - "description": "This structure stores parameters for a specific vesting schedule", - "type": "object", - "required": [ - "start_point" - ], - "properties": { - "end_point": { - "description": "The end point for the vesting schedule", - "anyOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - }, - { - "type": "null" - } - ] - }, - "start_point": { - "description": "The start date for the vesting schedule", - "allOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - } - ] - } - }, - "additionalProperties": false - }, - "VestingSchedulePoint": { - "description": "This structure stores the parameters used to create a vesting schedule.", - "type": "object", - "required": [ - "amount", - "time" - ], - "properties": { - "amount": { - "description": "The amount of tokens being vested", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "time": { - "description": "The start time for the vesting schedule", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } - } - } -} diff --git a/schemas/astroport-vesting/raw/execute.json b/schemas/astroport-vesting/raw/execute.json deleted file mode 100644 index 129698a57..000000000 --- a/schemas/astroport-vesting/raw/execute.json +++ /dev/null @@ -1,283 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This structure describes the execute messages available in the contract.", - "oneOf": [ - { - "description": "Claim claims vested tokens and sends them to a recipient", - "type": "object", - "required": [ - "claim" - ], - "properties": { - "claim": { - "type": "object", - "properties": { - "amount": { - "description": "The amount of tokens to claim", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "recipient": { - "description": "The address that receives the vested tokens", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template", - "type": "object", - "required": [ - "receive" - ], - "properties": { - "receive": { - "$ref": "#/definitions/Cw20ReceiveMsg" - } - }, - "additionalProperties": false - }, - { - "description": "RegisterVestingAccounts registers vesting targets/accounts", - "type": "object", - "required": [ - "register_vesting_accounts" - ], - "properties": { - "register_vesting_accounts": { - "type": "object", - "required": [ - "vesting_accounts" - ], - "properties": { - "vesting_accounts": { - "type": "array", - "items": { - "$ref": "#/definitions/VestingAccount" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Withdraws from current active schedule. Setups a new schedule with the remaining amount.", - "type": "object", - "required": [ - "withdraw_from_active_schedule" - ], - "properties": { - "withdraw_from_active_schedule": { - "type": "object", - "required": [ - "account", - "withdraw_amount" - ], - "properties": { - "account": { - "description": "The account from which tokens will be withdrawn", - "type": "string" - }, - "recipient": { - "description": "The address that receives the vested tokens", - "type": [ - "string", - "null" - ] - }, - "withdraw_amount": { - "description": "The amount of tokens to withdraw", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Creates a request to change contract ownership ## Executor Only the current owner can execute this", - "type": "object", - "required": [ - "propose_new_owner" - ], - "properties": { - "propose_new_owner": { - "type": "object", - "required": [ - "expires_in", - "owner" - ], - "properties": { - "expires_in": { - "description": "The validity period of the offer to change the owner", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "owner": { - "description": "The newly proposed owner", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes a request to change contract ownership ## Executor Only the current owner can execute this", - "type": "object", - "required": [ - "drop_ownership_proposal" - ], - "properties": { - "drop_ownership_proposal": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Claims contract ownership ## Executor Only the newly proposed owner can execute this", - "type": "object", - "required": [ - "claim_ownership" - ], - "properties": { - "claim_ownership": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20ReceiveMsg": { - "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", - "type": "object", - "required": [ - "amount", - "msg", - "sender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "sender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "VestingAccount": { - "description": "This structure stores vesting information for a specific address that is getting tokens.", - "type": "object", - "required": [ - "address", - "schedules" - ], - "properties": { - "address": { - "description": "The address that is getting tokens", - "type": "string" - }, - "schedules": { - "description": "The vesting schedules targeted at the `address`", - "type": "array", - "items": { - "$ref": "#/definitions/VestingSchedule" - } - } - }, - "additionalProperties": false - }, - "VestingSchedule": { - "description": "This structure stores parameters for a specific vesting schedule", - "type": "object", - "required": [ - "start_point" - ], - "properties": { - "end_point": { - "description": "The end point for the vesting schedule", - "anyOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - }, - { - "type": "null" - } - ] - }, - "start_point": { - "description": "The start date for the vesting schedule", - "allOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - } - ] - } - }, - "additionalProperties": false - }, - "VestingSchedulePoint": { - "description": "This structure stores the parameters used to create a vesting schedule.", - "type": "object", - "required": [ - "amount", - "time" - ], - "properties": { - "amount": { - "description": "The amount of tokens being vested", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "time": { - "description": "The start time for the vesting schedule", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-vesting/raw/instantiate.json b/schemas/astroport-vesting/raw/instantiate.json deleted file mode 100644 index 094d61f3a..000000000 --- a/schemas/astroport-vesting/raw/instantiate.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a contract.", - "type": "object", - "required": [ - "owner", - "vesting_token" - ], - "properties": { - "owner": { - "description": "Address allowed to change contract parameters", - "type": "string" - }, - "vesting_token": { - "description": "[`AssetInfo`] of the token that's being vested", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-vesting/raw/migrate.json b/schemas/astroport-vesting/raw/migrate.json deleted file mode 100644 index 196967242..000000000 --- a/schemas/astroport-vesting/raw/migrate.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "description": "This structure describes migration message.", - "type": "object", - "required": [ - "converter_contract" - ], - "properties": { - "converter_contract": { - "description": "Special migration message needed during the Hub move. Cw admin must be very cautious supplying correct converter contract.", - "type": "string" - } - }, - "additionalProperties": false -} diff --git a/schemas/astroport-vesting/raw/query.json b/schemas/astroport-vesting/raw/query.json deleted file mode 100644 index c7c1852c7..000000000 --- a/schemas/astroport-vesting/raw/query.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This structure describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Returns the configuration for the contract using a [`ConfigResponse`] object.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns information about an address vesting tokens using a [`VestingAccountResponse`] object.", - "type": "object", - "required": [ - "vesting_account" - ], - "properties": { - "vesting_account": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object.", - "type": "object", - "required": [ - "vesting_accounts" - ], - "properties": { - "vesting_accounts": { - "type": "object", - "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "order_by": { - "anyOf": [ - { - "$ref": "#/definitions/OrderBy" - }, - { - "type": "null" - } - ] - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the total unvested amount of tokens for a specific address.", - "type": "object", - "required": [ - "available_amount" - ], - "properties": { - "available_amount": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Timestamp returns the current timestamp", - "type": "object", - "required": [ - "timestamp" - ], - "properties": { - "timestamp": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "OrderBy": { - "description": "This enum describes the types of sorting that can be applied to some piece of data", - "type": "string", - "enum": [ - "asc", - "desc" - ] - } - } -} diff --git a/schemas/astroport-vesting/raw/response_to_available_amount.json b/schemas/astroport-vesting/raw/response_to_available_amount.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-vesting/raw/response_to_available_amount.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/schemas/astroport-vesting/raw/response_to_config.json b/schemas/astroport-vesting/raw/response_to_config.json deleted file mode 100644 index 543fb35a8..000000000 --- a/schemas/astroport-vesting/raw/response_to_config.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ConfigResponse", - "description": "This structure describes a custom struct used to return the contract configuration.", - "type": "object", - "required": [ - "owner", - "vesting_token" - ], - "properties": { - "owner": { - "description": "Address allowed to set contract parameters", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "vesting_token": { - "description": "[`AssetInfo`] of the token being vested", - "allOf": [ - { - "$ref": "#/definitions/AssetInfo" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "AssetInfo": { - "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", - "oneOf": [ - { - "description": "Non-native Token", - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Native token", - "type": "object", - "required": [ - "native_token" - ], - "properties": { - "native_token": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/schemas/astroport-vesting/raw/response_to_timestamp.json b/schemas/astroport-vesting/raw/response_to_timestamp.json deleted file mode 100644 index 7b729a7b9..000000000 --- a/schemas/astroport-vesting/raw/response_to_timestamp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "uint64", - "type": "integer", - "format": "uint64", - "minimum": 0.0 -} diff --git a/schemas/astroport-vesting/raw/response_to_vesting_account.json b/schemas/astroport-vesting/raw/response_to_vesting_account.json deleted file mode 100644 index d5bb1f361..000000000 --- a/schemas/astroport-vesting/raw/response_to_vesting_account.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "VestingAccountResponse", - "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", - "type": "object", - "required": [ - "address", - "info" - ], - "properties": { - "address": { - "description": "The address that's vesting tokens", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "info": { - "description": "Vesting information", - "allOf": [ - { - "$ref": "#/definitions/VestingInfo" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "VestingInfo": { - "description": "This structure stores parameters for a batch of vesting schedules.", - "type": "object", - "required": [ - "released_amount", - "schedules" - ], - "properties": { - "released_amount": { - "description": "The total amount of ASTRO already claimed", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "schedules": { - "description": "The vesting schedules", - "type": "array", - "items": { - "$ref": "#/definitions/VestingSchedule" - } - } - }, - "additionalProperties": false - }, - "VestingSchedule": { - "description": "This structure stores parameters for a specific vesting schedule", - "type": "object", - "required": [ - "start_point" - ], - "properties": { - "end_point": { - "description": "The end point for the vesting schedule", - "anyOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - }, - { - "type": "null" - } - ] - }, - "start_point": { - "description": "The start date for the vesting schedule", - "allOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - } - ] - } - }, - "additionalProperties": false - }, - "VestingSchedulePoint": { - "description": "This structure stores the parameters used to create a vesting schedule.", - "type": "object", - "required": [ - "amount", - "time" - ], - "properties": { - "amount": { - "description": "The amount of tokens being vested", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "time": { - "description": "The start time for the vesting schedule", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-vesting/raw/response_to_vesting_accounts.json b/schemas/astroport-vesting/raw/response_to_vesting_accounts.json deleted file mode 100644 index 3eda33d18..000000000 --- a/schemas/astroport-vesting/raw/response_to_vesting_accounts.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "VestingAccountsResponse", - "description": "This structure describes a custom struct used to return vesting data for multiple vesting targets.", - "type": "object", - "required": [ - "vesting_accounts" - ], - "properties": { - "vesting_accounts": { - "description": "A list of accounts that are vesting tokens", - "type": "array", - "items": { - "$ref": "#/definitions/VestingAccountResponse" - } - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "VestingAccountResponse": { - "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", - "type": "object", - "required": [ - "address", - "info" - ], - "properties": { - "address": { - "description": "The address that's vesting tokens", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "info": { - "description": "Vesting information", - "allOf": [ - { - "$ref": "#/definitions/VestingInfo" - } - ] - } - }, - "additionalProperties": false - }, - "VestingInfo": { - "description": "This structure stores parameters for a batch of vesting schedules.", - "type": "object", - "required": [ - "released_amount", - "schedules" - ], - "properties": { - "released_amount": { - "description": "The total amount of ASTRO already claimed", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "schedules": { - "description": "The vesting schedules", - "type": "array", - "items": { - "$ref": "#/definitions/VestingSchedule" - } - } - }, - "additionalProperties": false - }, - "VestingSchedule": { - "description": "This structure stores parameters for a specific vesting schedule", - "type": "object", - "required": [ - "start_point" - ], - "properties": { - "end_point": { - "description": "The end point for the vesting schedule", - "anyOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - }, - { - "type": "null" - } - ] - }, - "start_point": { - "description": "The start date for the vesting schedule", - "allOf": [ - { - "$ref": "#/definitions/VestingSchedulePoint" - } - ] - } - }, - "additionalProperties": false - }, - "VestingSchedulePoint": { - "description": "This structure stores the parameters used to create a vesting schedule.", - "type": "object", - "required": [ - "amount", - "time" - ], - "properties": { - "amount": { - "description": "The amount of tokens being vested", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "time": { - "description": "The start time for the vesting schedule", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } -} diff --git a/schemas/astroport-whitelist/astroport-whitelist.json b/schemas/astroport-whitelist/astroport-whitelist.json index f16c11415..064e383af 100644 --- a/schemas/astroport-whitelist/astroport-whitelist.json +++ b/schemas/astroport-whitelist/astroport-whitelist.json @@ -43,7 +43,7 @@ "msgs": { "type": "array", "items": { - "$ref": "#/definitions/CosmosMsg_for_NeutronMsg" + "$ref": "#/definitions/CosmosMsg_for_Empty" } } }, @@ -93,161 +93,6 @@ } ], "definitions": { - "AdminProposal": { - "description": "AdminProposal defines the struct for various proposals which Neutron's Admin Module may accept.", - "oneOf": [ - { - "description": "Proposal to change params. Note that this works for old params. New params has their own `MsgUpdateParams` msgs that can be supplied to `ProposalExecuteMessage`", - "type": "object", - "required": [ - "param_change_proposal" - ], - "properties": { - "param_change_proposal": { - "$ref": "#/definitions/ParamChangeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Proposal to upgrade IBC client", - "type": "object", - "required": [ - "upgrade_proposal" - ], - "properties": { - "upgrade_proposal": { - "$ref": "#/definitions/UpgradeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Proposal to update IBC client", - "type": "object", - "required": [ - "client_update_proposal" - ], - "properties": { - "client_update_proposal": { - "$ref": "#/definitions/ClientUpdateProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Proposal to execute CosmosMsg.", - "type": "object", - "required": [ - "proposal_execute_message" - ], - "properties": { - "proposal_execute_message": { - "$ref": "#/definitions/ProposalExecuteMessage" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to upgrade network", - "deprecated": true, - "type": "object", - "required": [ - "software_upgrade_proposal" - ], - "properties": { - "software_upgrade_proposal": { - "$ref": "#/definitions/SoftwareUpgradeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to cancel existing software upgrade", - "deprecated": true, - "type": "object", - "required": [ - "cancel_software_upgrade_proposal" - ], - "properties": { - "cancel_software_upgrade_proposal": { - "$ref": "#/definitions/CancelSoftwareUpgradeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Will fail to execute if you use it. Deprecated. Proposal to pin wasm contract codes", - "deprecated": true, - "type": "object", - "required": [ - "pin_codes_proposal" - ], - "properties": { - "pin_codes_proposal": { - "$ref": "#/definitions/PinCodesProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Deprecated. Proposal to unpin wasm contract codes.", - "deprecated": true, - "type": "object", - "required": [ - "unpin_codes_proposal" - ], - "properties": { - "unpin_codes_proposal": { - "$ref": "#/definitions/UnpinCodesProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to call sudo on contract.", - "deprecated": true, - "type": "object", - "required": [ - "sudo_contract_proposal" - ], - "properties": { - "sudo_contract_proposal": { - "$ref": "#/definitions/SudoContractProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to update contract admin.", - "deprecated": true, - "type": "object", - "required": [ - "update_admin_proposal" - ], - "properties": { - "update_admin_proposal": { - "$ref": "#/definitions/UpdateAdminProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to clear contract admin.", - "deprecated": true, - "type": "object", - "required": [ - "clear_admin_proposal" - ], - "properties": { - "clear_admin_proposal": { - "$ref": "#/definitions/ClearAdminProposal" - } - }, - "additionalProperties": false - } - ] - }, "BankMsg": { "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", "oneOf": [ @@ -309,77 +154,6 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, - "CancelSoftwareUpgradeProposal": { - "description": "Deprecated. CancelSoftwareUpgradeProposal defines the struct for cancel software upgrade proposal.", - "deprecated": true, - "type": "object", - "required": [ - "description", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal. Non unique.", - "type": "string" - } - } - }, - "ClearAdminProposal": { - "description": "Deprecated. SudoContractProposal defines the struct for clear admin proposal.", - "deprecated": true, - "type": "object", - "required": [ - "contract", - "description", - "title" - ], - "properties": { - "contract": { - "description": "*contract** is an address of contract admin will be removed.", - "type": "string" - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "ClientUpdateProposal": { - "description": "ClientUpdateProposal defines the struct for client update proposal.", - "type": "object", - "required": [ - "description", - "subject_client_id", - "substitute_client_id", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "subject_client_id": { - "description": "*subject_client_id** is a subject client id.", - "type": "string" - }, - "substitute_client_id": { - "description": "*substitute_client_id** is a substitute client id.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, "Coin": { "type": "object", "required": [ @@ -395,7 +169,7 @@ } } }, - "CosmosMsg_for_NeutronMsg": { + "CosmosMsg_for_Empty": { "oneOf": [ { "type": "object", @@ -416,7 +190,7 @@ ], "properties": { "custom": { - "$ref": "#/definitions/NeutronMsg" + "$ref": "#/definitions/Empty" } }, "additionalProperties": false @@ -508,31 +282,6 @@ } ] }, - "DenomUnit": { - "description": "Replicates the cosmos-sdk bank module DenomUnit type", - "type": "object", - "required": [ - "aliases", - "denom", - "exponent" - ], - "properties": { - "aliases": { - "type": "array", - "items": { - "type": "string" - } - }, - "denom": { - "type": "string" - }, - "exponent": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - } - }, "DistributionMsg": { "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", "oneOf": [ @@ -582,6 +331,10 @@ } ] }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, "GovMsg": { "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ @@ -619,38 +372,6 @@ } ] }, - "IbcFee": { - "description": "IbcFee defines struct for fees that refund the relayer for `SudoMsg` messages submission. Unused fee kind will be returned back to message sender. Please refer to these links for more information: IBC transaction structure - General mechanics of fee payments - ", - "type": "object", - "required": [ - "ack_fee", - "recv_fee", - "timeout_fee" - ], - "properties": { - "ack_fee": { - "description": "*ack_fee** is an amount of coins to refund relayer for submitting ack message for a particular IBC packet.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "recv_fee": { - "description": "**recv_fee** currently is used for compatibility with ICS-29 interface only and must be set to zero (i.e. 0untrn), because Neutron's fee module can't refund relayer for submission of Recv IBC packets due to compatibility with target chains.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "timeout_fee": { - "description": "*timeout_fee** amount of coins to refund relayer for submitting timeout message for a particular IBC packet.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - }, "IbcMsg": { "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", "oneOf": [ @@ -804,80 +525,28 @@ } } }, - "KVKey": { - "description": "Describes a KV key for which you want to get value from the storage on remote chain", - "type": "object", - "required": [ - "key", - "path" - ], - "properties": { - "key": { - "description": "*key** is a key you want to read from the storage", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "path": { - "description": "*path** is a path to the storage (storage prefix) where you want to read value by key (usually name of cosmos-packages module: 'staking', 'bank', etc.)", - "type": "string" - } - } - }, - "MsgExecuteContract": { - "description": "MsgExecuteContract defines a call to the contract execution", - "type": "object", - "required": [ - "contract", - "msg" - ], - "properties": { - "contract": { - "description": "*contract** is a contract address that will be called", - "type": "string" - }, - "msg": { - "description": "*msg** is a contract call message", - "type": "string" - } - } - }, - "NeutronMsg": { - "description": "A number of Custom messages that can call into the Neutron bindings.", + "StakingMsg": { + "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", "oneOf": [ { - "description": "RegisterInterchainAccount registers an interchain account on remote chain.", + "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", "type": "object", "required": [ - "register_interchain_account" + "delegate" ], "properties": { - "register_interchain_account": { + "delegate": { "type": "object", "required": [ - "connection_id", - "interchain_account_id" + "amount", + "validator" ], "properties": { - "connection_id": { - "description": "*connection_id** is an IBC connection identifier between Neutron and remote chain.", - "type": "string" + "amount": { + "$ref": "#/definitions/Coin" }, - "interchain_account_id": { - "description": "**interchain_account_id** is an identifier of your new interchain account. Can be any string. This identifier allows contracts to have multiple interchain accounts on remote chains.", + "validator": { "type": "string" - }, - "register_fee": { - "description": "*register_fee** is a fees required to be payed to register interchain account", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Coin" - } } } } @@ -885,55 +554,24 @@ "additionalProperties": false }, { - "description": "SubmitTx starts the process of executing any Cosmos-SDK *msgs* on remote chain.", + "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", "type": "object", "required": [ - "submit_tx" + "undelegate" ], "properties": { - "submit_tx": { + "undelegate": { "type": "object", "required": [ - "connection_id", - "fee", - "interchain_account_id", - "memo", - "msgs", - "timeout" + "amount", + "validator" ], "properties": { - "connection_id": { - "description": "*connection_id** is an IBC connection identifier between Neutron and remote chain.", - "type": "string" - }, - "fee": { - "description": "**fee** is an ibc fee for the transaction.", - "allOf": [ - { - "$ref": "#/definitions/IbcFee" - } - ] - }, - "interchain_account_id": { - "description": "*interchain_account_id** is an identifier of your interchain account from which you want to execute msgs.", - "type": "string" + "amount": { + "$ref": "#/definitions/Coin" }, - "memo": { - "description": "*memo** is a memo you want to attach to your interchain transaction.It behaves like a memo in usual Cosmos transaction.", + "validator": { "type": "string" - }, - "msgs": { - "description": "*msgs** is a list of protobuf encoded Cosmos-SDK messages you want to execute on remote chain.", - "type": "array", - "items": { - "$ref": "#/definitions/ProtobufAny" - } - }, - "timeout": { - "description": "*timeout** is a timeout in seconds after which the packet times out.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -941,769 +579,27 @@ "additionalProperties": false }, { - "description": "RegisterInterchainQuery registers an interchain query.", + "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", "type": "object", "required": [ - "register_interchain_query" + "redelegate" ], "properties": { - "register_interchain_query": { + "redelegate": { "type": "object", "required": [ - "connection_id", - "keys", - "query_type", - "transactions_filter", - "update_period" + "amount", + "dst_validator", + "src_validator" ], "properties": { - "connection_id": { - "description": "*connection_id** is an IBC connection identifier between Neutron and remote chain.", - "type": "string" - }, - "keys": { - "description": "*keys** is the KV-storage keys for which we want to get values from remote chain.", - "type": "array", - "items": { - "$ref": "#/definitions/KVKey" - } + "amount": { + "$ref": "#/definitions/Coin" }, - "query_type": { - "description": "*query_type** is a query type identifier ('tx' or 'kv' for now).", + "dst_validator": { "type": "string" }, - "transactions_filter": { - "description": "*transactions_filter** is the filter for transaction search ICQ.", - "type": "string" - }, - "update_period": { - "description": "*update_period** is used to say how often the query must be updated.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "RegisterInterchainQuery updates an interchain query.", - "type": "object", - "required": [ - "update_interchain_query" - ], - "properties": { - "update_interchain_query": { - "type": "object", - "required": [ - "query_id" - ], - "properties": { - "new_keys": { - "description": "*new_keys** is the new query keys to retrive.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/KVKey" - } - }, - "new_transactions_filter": { - "description": "*new_transactions_filter** is a new transactions filter of the query.", - "type": [ - "string", - "null" - ] - }, - "new_update_period": { - "description": "*new_update_period** is a new update period of the query.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "query_id": { - "description": "*query_id** is the ID of the query we want to update.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "RemoveInterchainQuery removes as interchain query.", - "type": "object", - "required": [ - "remove_interchain_query" - ], - "properties": { - "remove_interchain_query": { - "type": "object", - "required": [ - "query_id" - ], - "properties": { - "query_id": { - "description": "*query_id** is ID of the query we want to remove.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "IbcTransfer sends a fungible token packet over IBC.", - "type": "object", - "required": [ - "ibc_transfer" - ], - "properties": { - "ibc_transfer": { - "type": "object", - "required": [ - "fee", - "memo", - "receiver", - "sender", - "source_channel", - "source_port", - "timeout_height", - "timeout_timestamp", - "token" - ], - "properties": { - "fee": { - "$ref": "#/definitions/IbcFee" - }, - "memo": { - "type": "string" - }, - "receiver": { - "type": "string" - }, - "sender": { - "type": "string" - }, - "source_channel": { - "type": "string" - }, - "source_port": { - "type": "string" - }, - "timeout_height": { - "$ref": "#/definitions/RequestPacketTimeoutHeight" - }, - "timeout_timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "token": { - "$ref": "#/definitions/Coin" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "SubmitAdminProposal sends a proposal to neutron's Admin module. This type of messages can be only executed by Neutron DAO.", - "type": "object", - "required": [ - "submit_admin_proposal" - ], - "properties": { - "submit_admin_proposal": { - "type": "object", - "required": [ - "admin_proposal" - ], - "properties": { - "admin_proposal": { - "$ref": "#/definitions/AdminProposal" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can create denoms, namespaced under the contract's address. A contract may create any number of independent sub-denoms.", - "type": "object", - "required": [ - "create_denom" - ], - "properties": { - "create_denom": { - "type": "object", - "required": [ - "subdenom" - ], - "properties": { - "subdenom": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can change the admin of a denom that they are the admin of.", - "type": "object", - "required": [ - "change_admin" - ], - "properties": { - "change_admin": { - "type": "object", - "required": [ - "denom", - "new_admin_address" - ], - "properties": { - "denom": { - "type": "string" - }, - "new_admin_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can mint native tokens for an existing factory denom that they are the admin of.", - "type": "object", - "required": [ - "mint_tokens" - ], - "properties": { - "mint_tokens": { - "type": "object", - "required": [ - "amount", - "denom", - "mint_to_address" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - }, - "mint_to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can burn native tokens for an existing factory denom that they are the admin of. Currently, the burn from address must be the admin contract.", - "type": "object", - "required": [ - "burn_tokens" - ], - "properties": { - "burn_tokens": { - "type": "object", - "required": [ - "amount", - "burn_from_address", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "burn_from_address": { - "description": "Must be set to `\"\"` for now", - "type": "string" - }, - "denom": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can set before send hooks for denoms, namespaced under the contract's address.", - "type": "object", - "required": [ - "set_before_send_hook" - ], - "properties": { - "set_before_send_hook": { - "type": "object", - "required": [ - "contract_addr", - "denom" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "denom": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactoryMessage Contracts can force specified `amount` of an existing factory denom that they are admin of to a `transfer_to_address` from a `transfer_from_address`.", - "type": "object", - "required": [ - "force_transfer" - ], - "properties": { - "force_transfer": { - "type": "object", - "required": [ - "amount", - "denom", - "transfer_from_address", - "transfer_to_address" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - }, - "transfer_from_address": { - "type": "string" - }, - "transfer_to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactoryMessage Contracts can set a metadata for of an existing factory denom that they are admin of.", - "type": "object", - "required": [ - "set_denom_metadata" - ], - "properties": { - "set_denom_metadata": { - "type": "object", - "required": [ - "base", - "denom_units", - "description", - "display", - "name", - "symbol", - "uri", - "uri_hash" - ], - "properties": { - "base": { - "description": "*base** represents the base denom (should be the DenomUnit with exponent = 0).", - "type": "string" - }, - "denom_units": { - "description": "*denom_units** represents the list of DenomUnit's for a given coin", - "type": "array", - "items": { - "$ref": "#/definitions/DenomUnit" - } - }, - "description": { - "description": "*description** description of a token", - "type": "string" - }, - "display": { - "description": "**display** indicates the suggested denom that should be displayed in clients.", - "type": "string" - }, - "name": { - "description": "*name** defines the name of the token (eg: Cosmos Atom)", - "type": "string" - }, - "symbol": { - "description": "**symbol** is the token symbol usually shown on exchanges (eg: ATOM). This can be the same as the display.", - "type": "string" - }, - "uri": { - "description": "*uri** to a document (on or off-chain) that contains additional information. Optional.", - "type": "string" - }, - "uri_hash": { - "description": "**uri_hash** is a sha256 hash of a document pointed by URI. It's used to verify that the document didn't change. Optional.", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "AddSchedule adds new schedule with a given `name`. Until schedule is removed it will execute all `msgs` every `period` blocks. First execution is at least on `current_block + period` block. [Permissioned - DAO Only]", - "type": "object", - "required": [ - "add_schedule" - ], - "properties": { - "add_schedule": { - "type": "object", - "required": [ - "msgs", - "name", - "period" - ], - "properties": { - "msgs": { - "description": "list of cosmwasm messages to be executed", - "type": "array", - "items": { - "$ref": "#/definitions/MsgExecuteContract" - } - }, - "name": { - "description": "Name of a new schedule. Needed to be able to `RemoveSchedule` and to log information about it", - "type": "string" - }, - "period": { - "description": "period in blocks with which `msgs` will be executed", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "RemoveSchedule removes the schedule with a given `name`. [Permissioned - DAO or Security DAO only]", - "type": "object", - "required": [ - "remove_schedule" - ], - "properties": { - "remove_schedule": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Contractmanager message Resubmits failed acknowledgement. Acknowledgement failure is created when contract returns error or acknowledgement is out of gas. [Permissioned - only from contract that is initial caller of IBC transaction]", - "type": "object", - "required": [ - "resubmit_failure" - ], - "properties": { - "resubmit_failure": { - "type": "object", - "required": [ - "failure_id" - ], - "properties": { - "failure_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - } - ] - }, - "ParamChange": { - "description": "ParamChange defines the struct for parameter change request.", - "type": "object", - "required": [ - "key", - "subspace", - "value" - ], - "properties": { - "key": { - "description": "*key** is a name of parameter. Unique for subspace.", - "type": "string" - }, - "subspace": { - "description": "*subspace** is a key of module to which the parameter to change belongs. Unique for each module.", - "type": "string" - }, - "value": { - "description": "*value** is a new value for given parameter. Non unique.", - "type": "string" - } - } - }, - "ParamChangeProposal": { - "description": "ParamChangeProposal defines the struct for single parameter change proposal.", - "type": "object", - "required": [ - "description", - "param_changes", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "param_changes": { - "description": "*param_changes** is a vector of params to be changed. Non unique.", - "type": "array", - "items": { - "$ref": "#/definitions/ParamChange" - } - }, - "title": { - "description": "*title** is a text title of proposal. Non unique.", - "type": "string" - } - } - }, - "PinCodesProposal": { - "description": "Deprecated. PinCodesProposal defines the struct for pin contract codes proposal.", - "deprecated": true, - "type": "object", - "required": [ - "code_ids", - "description", - "title" - ], - "properties": { - "code_ids": { - "description": "*code_ids** is an array of codes to be pined.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "Plan": { - "description": "Plan defines the struct for planned upgrade.", - "type": "object", - "required": [ - "height", - "info", - "name" - ], - "properties": { - "height": { - "description": "*height** is a height at which the upgrade must be performed", - "type": "integer", - "format": "int64" - }, - "info": { - "description": "*info** is any application specific upgrade info to be included on-chain", - "type": "string" - }, - "name": { - "description": "*name** is a name for the upgrade", - "type": "string" - } - } - }, - "ProposalExecuteMessage": { - "description": "ProposalExecuteMessage defines the struct for sdk47 compatible admin proposal.", - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "description": "*message** is a json representing an sdk message passed to admin module to execute.", - "type": "string" - } - } - }, - "ProtobufAny": { - "description": "Type for wrapping any protobuf message", - "type": "object", - "required": [ - "type_url", - "value" - ], - "properties": { - "type_url": { - "description": "*type_url** describes the type of the serialized message", - "type": "string" - }, - "value": { - "description": "*value** must be a valid serialized protocol buffer of the above specified type", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - }, - "RequestPacketTimeoutHeight": { - "type": "object", - "properties": { - "revision_height": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "revision_number": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - } - }, - "SoftwareUpgradeProposal": { - "description": "Deprecated. SoftwareUpgradeProposal defines the struct for software upgrade proposal.", - "deprecated": true, - "type": "object", - "required": [ - "description", - "plan", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "plan": { - "description": "*plan** is a plan of upgrade.", - "allOf": [ - { - "$ref": "#/definitions/Plan" - } - ] - }, - "title": { - "description": "*title** is a text title of proposal. Non unique.", - "type": "string" - } - } - }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { + "src_validator": { "type": "string" } } @@ -1713,39 +609,6 @@ } ] }, - "SudoContractProposal": { - "description": "Deprecated. SudoContractProposal defines the struct for sudo execution proposal.", - "deprecated": true, - "type": "object", - "required": [ - "contract", - "description", - "msg", - "title" - ], - "properties": { - "contract": { - "description": "*contract** is an address of contract to be executed.", - "type": "string" - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "msg": { - "description": "**msg*** is a sudo message.", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ @@ -1762,100 +625,6 @@ "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", "type": "string" }, - "UnpinCodesProposal": { - "description": "Deprecated. UnpinCodesProposal defines the struct for unpin contract codes proposal.", - "deprecated": true, - "type": "object", - "required": [ - "code_ids", - "description", - "title" - ], - "properties": { - "code_ids": { - "description": "*code_ids** is an array of codes to be unpined.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "UpdateAdminProposal": { - "description": "Deprecated. UpdateAdminProposal defines the struct for update admin proposal.", - "deprecated": true, - "type": "object", - "required": [ - "contract", - "description", - "new_admin", - "title" - ], - "properties": { - "contract": { - "description": "*contract** is an address of contract to update admin.", - "type": "string" - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "new_admin": { - "description": "**new_admin*** is an address of new admin", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "UpgradeProposal": { - "description": "UpgradeProposal defines the struct for IBC upgrade proposal.", - "type": "object", - "required": [ - "description", - "plan", - "title", - "upgraded_client_state" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "plan": { - "description": "*plan** is a plan of upgrade.", - "allOf": [ - { - "$ref": "#/definitions/Plan" - } - ] - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - }, - "upgraded_client_state": { - "description": "*upgraded_client_state** is an upgraded client state.", - "allOf": [ - { - "$ref": "#/definitions/ProtobufAny" - } - ] - } - } - }, "VoteOption": { "type": "string", "enum": [ diff --git a/schemas/astroport-whitelist/raw/execute.json b/schemas/astroport-whitelist/raw/execute.json index 922c7449d..6d204f2de 100644 --- a/schemas/astroport-whitelist/raw/execute.json +++ b/schemas/astroport-whitelist/raw/execute.json @@ -18,7 +18,7 @@ "msgs": { "type": "array", "items": { - "$ref": "#/definitions/CosmosMsg_for_NeutronMsg" + "$ref": "#/definitions/CosmosMsg_for_Empty" } } }, @@ -68,161 +68,6 @@ } ], "definitions": { - "AdminProposal": { - "description": "AdminProposal defines the struct for various proposals which Neutron's Admin Module may accept.", - "oneOf": [ - { - "description": "Proposal to change params. Note that this works for old params. New params has their own `MsgUpdateParams` msgs that can be supplied to `ProposalExecuteMessage`", - "type": "object", - "required": [ - "param_change_proposal" - ], - "properties": { - "param_change_proposal": { - "$ref": "#/definitions/ParamChangeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Proposal to upgrade IBC client", - "type": "object", - "required": [ - "upgrade_proposal" - ], - "properties": { - "upgrade_proposal": { - "$ref": "#/definitions/UpgradeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Proposal to update IBC client", - "type": "object", - "required": [ - "client_update_proposal" - ], - "properties": { - "client_update_proposal": { - "$ref": "#/definitions/ClientUpdateProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Proposal to execute CosmosMsg.", - "type": "object", - "required": [ - "proposal_execute_message" - ], - "properties": { - "proposal_execute_message": { - "$ref": "#/definitions/ProposalExecuteMessage" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to upgrade network", - "deprecated": true, - "type": "object", - "required": [ - "software_upgrade_proposal" - ], - "properties": { - "software_upgrade_proposal": { - "$ref": "#/definitions/SoftwareUpgradeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to cancel existing software upgrade", - "deprecated": true, - "type": "object", - "required": [ - "cancel_software_upgrade_proposal" - ], - "properties": { - "cancel_software_upgrade_proposal": { - "$ref": "#/definitions/CancelSoftwareUpgradeProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Will fail to execute if you use it. Deprecated. Proposal to pin wasm contract codes", - "deprecated": true, - "type": "object", - "required": [ - "pin_codes_proposal" - ], - "properties": { - "pin_codes_proposal": { - "$ref": "#/definitions/PinCodesProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Deprecated. Proposal to unpin wasm contract codes.", - "deprecated": true, - "type": "object", - "required": [ - "unpin_codes_proposal" - ], - "properties": { - "unpin_codes_proposal": { - "$ref": "#/definitions/UnpinCodesProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to call sudo on contract.", - "deprecated": true, - "type": "object", - "required": [ - "sudo_contract_proposal" - ], - "properties": { - "sudo_contract_proposal": { - "$ref": "#/definitions/SudoContractProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to update contract admin.", - "deprecated": true, - "type": "object", - "required": [ - "update_admin_proposal" - ], - "properties": { - "update_admin_proposal": { - "$ref": "#/definitions/UpdateAdminProposal" - } - }, - "additionalProperties": false - }, - { - "description": "Deprecated. Proposal to clear contract admin.", - "deprecated": true, - "type": "object", - "required": [ - "clear_admin_proposal" - ], - "properties": { - "clear_admin_proposal": { - "$ref": "#/definitions/ClearAdminProposal" - } - }, - "additionalProperties": false - } - ] - }, "BankMsg": { "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", "oneOf": [ @@ -284,77 +129,6 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, - "CancelSoftwareUpgradeProposal": { - "description": "Deprecated. CancelSoftwareUpgradeProposal defines the struct for cancel software upgrade proposal.", - "deprecated": true, - "type": "object", - "required": [ - "description", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal. Non unique.", - "type": "string" - } - } - }, - "ClearAdminProposal": { - "description": "Deprecated. SudoContractProposal defines the struct for clear admin proposal.", - "deprecated": true, - "type": "object", - "required": [ - "contract", - "description", - "title" - ], - "properties": { - "contract": { - "description": "*contract** is an address of contract admin will be removed.", - "type": "string" - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "ClientUpdateProposal": { - "description": "ClientUpdateProposal defines the struct for client update proposal.", - "type": "object", - "required": [ - "description", - "subject_client_id", - "substitute_client_id", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "subject_client_id": { - "description": "*subject_client_id** is a subject client id.", - "type": "string" - }, - "substitute_client_id": { - "description": "*substitute_client_id** is a substitute client id.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, "Coin": { "type": "object", "required": [ @@ -370,7 +144,7 @@ } } }, - "CosmosMsg_for_NeutronMsg": { + "CosmosMsg_for_Empty": { "oneOf": [ { "type": "object", @@ -391,7 +165,7 @@ ], "properties": { "custom": { - "$ref": "#/definitions/NeutronMsg" + "$ref": "#/definitions/Empty" } }, "additionalProperties": false @@ -483,31 +257,6 @@ } ] }, - "DenomUnit": { - "description": "Replicates the cosmos-sdk bank module DenomUnit type", - "type": "object", - "required": [ - "aliases", - "denom", - "exponent" - ], - "properties": { - "aliases": { - "type": "array", - "items": { - "type": "string" - } - }, - "denom": { - "type": "string" - }, - "exponent": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - } - }, "DistributionMsg": { "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", "oneOf": [ @@ -557,6 +306,10 @@ } ] }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, "GovMsg": { "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ @@ -594,38 +347,6 @@ } ] }, - "IbcFee": { - "description": "IbcFee defines struct for fees that refund the relayer for `SudoMsg` messages submission. Unused fee kind will be returned back to message sender. Please refer to these links for more information: IBC transaction structure - General mechanics of fee payments - ", - "type": "object", - "required": [ - "ack_fee", - "recv_fee", - "timeout_fee" - ], - "properties": { - "ack_fee": { - "description": "*ack_fee** is an amount of coins to refund relayer for submitting ack message for a particular IBC packet.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "recv_fee": { - "description": "**recv_fee** currently is used for compatibility with ICS-29 interface only and must be set to zero (i.e. 0untrn), because Neutron's fee module can't refund relayer for submission of Recv IBC packets due to compatibility with target chains.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "timeout_fee": { - "description": "*timeout_fee** amount of coins to refund relayer for submitting timeout message for a particular IBC packet.", - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - }, "IbcMsg": { "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", "oneOf": [ @@ -779,80 +500,28 @@ } } }, - "KVKey": { - "description": "Describes a KV key for which you want to get value from the storage on remote chain", - "type": "object", - "required": [ - "key", - "path" - ], - "properties": { - "key": { - "description": "*key** is a key you want to read from the storage", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "path": { - "description": "*path** is a path to the storage (storage prefix) where you want to read value by key (usually name of cosmos-packages module: 'staking', 'bank', etc.)", - "type": "string" - } - } - }, - "MsgExecuteContract": { - "description": "MsgExecuteContract defines a call to the contract execution", - "type": "object", - "required": [ - "contract", - "msg" - ], - "properties": { - "contract": { - "description": "*contract** is a contract address that will be called", - "type": "string" - }, - "msg": { - "description": "*msg** is a contract call message", - "type": "string" - } - } - }, - "NeutronMsg": { - "description": "A number of Custom messages that can call into the Neutron bindings.", + "StakingMsg": { + "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", "oneOf": [ { - "description": "RegisterInterchainAccount registers an interchain account on remote chain.", + "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", "type": "object", "required": [ - "register_interchain_account" + "delegate" ], "properties": { - "register_interchain_account": { + "delegate": { "type": "object", "required": [ - "connection_id", - "interchain_account_id" + "amount", + "validator" ], "properties": { - "connection_id": { - "description": "*connection_id** is an IBC connection identifier between Neutron and remote chain.", - "type": "string" + "amount": { + "$ref": "#/definitions/Coin" }, - "interchain_account_id": { - "description": "**interchain_account_id** is an identifier of your new interchain account. Can be any string. This identifier allows contracts to have multiple interchain accounts on remote chains.", + "validator": { "type": "string" - }, - "register_fee": { - "description": "*register_fee** is a fees required to be payed to register interchain account", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Coin" - } } } } @@ -860,55 +529,24 @@ "additionalProperties": false }, { - "description": "SubmitTx starts the process of executing any Cosmos-SDK *msgs* on remote chain.", + "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", "type": "object", "required": [ - "submit_tx" + "undelegate" ], "properties": { - "submit_tx": { + "undelegate": { "type": "object", "required": [ - "connection_id", - "fee", - "interchain_account_id", - "memo", - "msgs", - "timeout" + "amount", + "validator" ], "properties": { - "connection_id": { - "description": "*connection_id** is an IBC connection identifier between Neutron and remote chain.", - "type": "string" - }, - "fee": { - "description": "**fee** is an ibc fee for the transaction.", - "allOf": [ - { - "$ref": "#/definitions/IbcFee" - } - ] - }, - "interchain_account_id": { - "description": "*interchain_account_id** is an identifier of your interchain account from which you want to execute msgs.", - "type": "string" + "amount": { + "$ref": "#/definitions/Coin" }, - "memo": { - "description": "*memo** is a memo you want to attach to your interchain transaction.It behaves like a memo in usual Cosmos transaction.", + "validator": { "type": "string" - }, - "msgs": { - "description": "*msgs** is a list of protobuf encoded Cosmos-SDK messages you want to execute on remote chain.", - "type": "array", - "items": { - "$ref": "#/definitions/ProtobufAny" - } - }, - "timeout": { - "description": "*timeout** is a timeout in seconds after which the packet times out.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -916,814 +554,39 @@ "additionalProperties": false }, { - "description": "RegisterInterchainQuery registers an interchain query.", + "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", "type": "object", "required": [ - "register_interchain_query" + "redelegate" ], "properties": { - "register_interchain_query": { + "redelegate": { "type": "object", "required": [ - "connection_id", - "keys", - "query_type", - "transactions_filter", - "update_period" + "amount", + "dst_validator", + "src_validator" ], "properties": { - "connection_id": { - "description": "*connection_id** is an IBC connection identifier between Neutron and remote chain.", - "type": "string" - }, - "keys": { - "description": "*keys** is the KV-storage keys for which we want to get values from remote chain.", - "type": "array", - "items": { - "$ref": "#/definitions/KVKey" - } + "amount": { + "$ref": "#/definitions/Coin" }, - "query_type": { - "description": "*query_type** is a query type identifier ('tx' or 'kv' for now).", + "dst_validator": { "type": "string" }, - "transactions_filter": { - "description": "*transactions_filter** is the filter for transaction search ICQ.", + "src_validator": { "type": "string" - }, - "update_period": { - "description": "*update_period** is used to say how often the query must be updated.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } }, "additionalProperties": false - }, - { - "description": "RegisterInterchainQuery updates an interchain query.", - "type": "object", - "required": [ - "update_interchain_query" - ], - "properties": { - "update_interchain_query": { - "type": "object", - "required": [ - "query_id" - ], - "properties": { - "new_keys": { - "description": "*new_keys** is the new query keys to retrive.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/KVKey" - } - }, - "new_transactions_filter": { - "description": "*new_transactions_filter** is a new transactions filter of the query.", - "type": [ - "string", - "null" - ] - }, - "new_update_period": { - "description": "*new_update_period** is a new update period of the query.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "query_id": { - "description": "*query_id** is the ID of the query we want to update.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "RemoveInterchainQuery removes as interchain query.", - "type": "object", - "required": [ - "remove_interchain_query" - ], - "properties": { - "remove_interchain_query": { - "type": "object", - "required": [ - "query_id" - ], - "properties": { - "query_id": { - "description": "*query_id** is ID of the query we want to remove.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "IbcTransfer sends a fungible token packet over IBC.", - "type": "object", - "required": [ - "ibc_transfer" - ], - "properties": { - "ibc_transfer": { - "type": "object", - "required": [ - "fee", - "memo", - "receiver", - "sender", - "source_channel", - "source_port", - "timeout_height", - "timeout_timestamp", - "token" - ], - "properties": { - "fee": { - "$ref": "#/definitions/IbcFee" - }, - "memo": { - "type": "string" - }, - "receiver": { - "type": "string" - }, - "sender": { - "type": "string" - }, - "source_channel": { - "type": "string" - }, - "source_port": { - "type": "string" - }, - "timeout_height": { - "$ref": "#/definitions/RequestPacketTimeoutHeight" - }, - "timeout_timestamp": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "token": { - "$ref": "#/definitions/Coin" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "SubmitAdminProposal sends a proposal to neutron's Admin module. This type of messages can be only executed by Neutron DAO.", - "type": "object", - "required": [ - "submit_admin_proposal" - ], - "properties": { - "submit_admin_proposal": { - "type": "object", - "required": [ - "admin_proposal" - ], - "properties": { - "admin_proposal": { - "$ref": "#/definitions/AdminProposal" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can create denoms, namespaced under the contract's address. A contract may create any number of independent sub-denoms.", - "type": "object", - "required": [ - "create_denom" - ], - "properties": { - "create_denom": { - "type": "object", - "required": [ - "subdenom" - ], - "properties": { - "subdenom": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can change the admin of a denom that they are the admin of.", - "type": "object", - "required": [ - "change_admin" - ], - "properties": { - "change_admin": { - "type": "object", - "required": [ - "denom", - "new_admin_address" - ], - "properties": { - "denom": { - "type": "string" - }, - "new_admin_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can mint native tokens for an existing factory denom that they are the admin of.", - "type": "object", - "required": [ - "mint_tokens" - ], - "properties": { - "mint_tokens": { - "type": "object", - "required": [ - "amount", - "denom", - "mint_to_address" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - }, - "mint_to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can burn native tokens for an existing factory denom that they are the admin of. Currently, the burn from address must be the admin contract.", - "type": "object", - "required": [ - "burn_tokens" - ], - "properties": { - "burn_tokens": { - "type": "object", - "required": [ - "amount", - "burn_from_address", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "burn_from_address": { - "description": "Must be set to `\"\"` for now", - "type": "string" - }, - "denom": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactory message. Contracts can set before send hooks for denoms, namespaced under the contract's address.", - "type": "object", - "required": [ - "set_before_send_hook" - ], - "properties": { - "set_before_send_hook": { - "type": "object", - "required": [ - "contract_addr", - "denom" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "denom": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactoryMessage Contracts can force specified `amount` of an existing factory denom that they are admin of to a `transfer_to_address` from a `transfer_from_address`.", - "type": "object", - "required": [ - "force_transfer" - ], - "properties": { - "force_transfer": { - "type": "object", - "required": [ - "amount", - "denom", - "transfer_from_address", - "transfer_to_address" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - }, - "transfer_from_address": { - "type": "string" - }, - "transfer_to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "TokenFactoryMessage Contracts can set a metadata for of an existing factory denom that they are admin of.", - "type": "object", - "required": [ - "set_denom_metadata" - ], - "properties": { - "set_denom_metadata": { - "type": "object", - "required": [ - "base", - "denom_units", - "description", - "display", - "name", - "symbol", - "uri", - "uri_hash" - ], - "properties": { - "base": { - "description": "*base** represents the base denom (should be the DenomUnit with exponent = 0).", - "type": "string" - }, - "denom_units": { - "description": "*denom_units** represents the list of DenomUnit's for a given coin", - "type": "array", - "items": { - "$ref": "#/definitions/DenomUnit" - } - }, - "description": { - "description": "*description** description of a token", - "type": "string" - }, - "display": { - "description": "**display** indicates the suggested denom that should be displayed in clients.", - "type": "string" - }, - "name": { - "description": "*name** defines the name of the token (eg: Cosmos Atom)", - "type": "string" - }, - "symbol": { - "description": "**symbol** is the token symbol usually shown on exchanges (eg: ATOM). This can be the same as the display.", - "type": "string" - }, - "uri": { - "description": "*uri** to a document (on or off-chain) that contains additional information. Optional.", - "type": "string" - }, - "uri_hash": { - "description": "**uri_hash** is a sha256 hash of a document pointed by URI. It's used to verify that the document didn't change. Optional.", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "AddSchedule adds new schedule with a given `name`. Until schedule is removed it will execute all `msgs` every `period` blocks. First execution is at least on `current_block + period` block. [Permissioned - DAO Only]", - "type": "object", - "required": [ - "add_schedule" - ], - "properties": { - "add_schedule": { - "type": "object", - "required": [ - "msgs", - "name", - "period" - ], - "properties": { - "msgs": { - "description": "list of cosmwasm messages to be executed", - "type": "array", - "items": { - "$ref": "#/definitions/MsgExecuteContract" - } - }, - "name": { - "description": "Name of a new schedule. Needed to be able to `RemoveSchedule` and to log information about it", - "type": "string" - }, - "period": { - "description": "period in blocks with which `msgs` will be executed", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "RemoveSchedule removes the schedule with a given `name`. [Permissioned - DAO or Security DAO only]", - "type": "object", - "required": [ - "remove_schedule" - ], - "properties": { - "remove_schedule": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Contractmanager message Resubmits failed acknowledgement. Acknowledgement failure is created when contract returns error or acknowledgement is out of gas. [Permissioned - only from contract that is initial caller of IBC transaction]", - "type": "object", - "required": [ - "resubmit_failure" - ], - "properties": { - "resubmit_failure": { - "type": "object", - "required": [ - "failure_id" - ], - "properties": { - "failure_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - } - ] - }, - "ParamChange": { - "description": "ParamChange defines the struct for parameter change request.", - "type": "object", - "required": [ - "key", - "subspace", - "value" - ], - "properties": { - "key": { - "description": "*key** is a name of parameter. Unique for subspace.", - "type": "string" - }, - "subspace": { - "description": "*subspace** is a key of module to which the parameter to change belongs. Unique for each module.", - "type": "string" - }, - "value": { - "description": "*value** is a new value for given parameter. Non unique.", - "type": "string" - } - } - }, - "ParamChangeProposal": { - "description": "ParamChangeProposal defines the struct for single parameter change proposal.", - "type": "object", - "required": [ - "description", - "param_changes", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "param_changes": { - "description": "*param_changes** is a vector of params to be changed. Non unique.", - "type": "array", - "items": { - "$ref": "#/definitions/ParamChange" - } - }, - "title": { - "description": "*title** is a text title of proposal. Non unique.", - "type": "string" - } - } - }, - "PinCodesProposal": { - "description": "Deprecated. PinCodesProposal defines the struct for pin contract codes proposal.", - "deprecated": true, - "type": "object", - "required": [ - "code_ids", - "description", - "title" - ], - "properties": { - "code_ids": { - "description": "*code_ids** is an array of codes to be pined.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "Plan": { - "description": "Plan defines the struct for planned upgrade.", - "type": "object", - "required": [ - "height", - "info", - "name" - ], - "properties": { - "height": { - "description": "*height** is a height at which the upgrade must be performed", - "type": "integer", - "format": "int64" - }, - "info": { - "description": "*info** is any application specific upgrade info to be included on-chain", - "type": "string" - }, - "name": { - "description": "*name** is a name for the upgrade", - "type": "string" - } - } - }, - "ProposalExecuteMessage": { - "description": "ProposalExecuteMessage defines the struct for sdk47 compatible admin proposal.", - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "description": "*message** is a json representing an sdk message passed to admin module to execute.", - "type": "string" - } - } - }, - "ProtobufAny": { - "description": "Type for wrapping any protobuf message", - "type": "object", - "required": [ - "type_url", - "value" - ], - "properties": { - "type_url": { - "description": "*type_url** describes the type of the serialized message", - "type": "string" - }, - "value": { - "description": "*value** must be a valid serialized protocol buffer of the above specified type", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - }, - "RequestPacketTimeoutHeight": { - "type": "object", - "properties": { - "revision_height": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "revision_number": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - } - }, - "SoftwareUpgradeProposal": { - "description": "Deprecated. SoftwareUpgradeProposal defines the struct for software upgrade proposal.", - "deprecated": true, - "type": "object", - "required": [ - "description", - "plan", - "title" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal. Non unique.", - "type": "string" - }, - "plan": { - "description": "*plan** is a plan of upgrade.", - "allOf": [ - { - "$ref": "#/definitions/Plan" - } - ] - }, - "title": { - "description": "*title** is a text title of proposal. Non unique.", - "type": "string" - } - } - }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "SudoContractProposal": { - "description": "Deprecated. SudoContractProposal defines the struct for sudo execution proposal.", - "deprecated": true, - "type": "object", - "required": [ - "contract", - "description", - "msg", - "title" - ], - "properties": { - "contract": { - "description": "*contract** is an address of contract to be executed.", - "type": "string" - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "msg": { - "description": "**msg*** is a sudo message.", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ { "$ref": "#/definitions/Uint64" } @@ -1737,100 +600,6 @@ "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", "type": "string" }, - "UnpinCodesProposal": { - "description": "Deprecated. UnpinCodesProposal defines the struct for unpin contract codes proposal.", - "deprecated": true, - "type": "object", - "required": [ - "code_ids", - "description", - "title" - ], - "properties": { - "code_ids": { - "description": "*code_ids** is an array of codes to be unpined.", - "type": "array", - "items": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "UpdateAdminProposal": { - "description": "Deprecated. UpdateAdminProposal defines the struct for update admin proposal.", - "deprecated": true, - "type": "object", - "required": [ - "contract", - "description", - "new_admin", - "title" - ], - "properties": { - "contract": { - "description": "*contract** is an address of contract to update admin.", - "type": "string" - }, - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "new_admin": { - "description": "**new_admin*** is an address of new admin", - "type": "string" - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - } - } - }, - "UpgradeProposal": { - "description": "UpgradeProposal defines the struct for IBC upgrade proposal.", - "type": "object", - "required": [ - "description", - "plan", - "title", - "upgraded_client_state" - ], - "properties": { - "description": { - "description": "*description** is a text description of proposal.", - "type": "string" - }, - "plan": { - "description": "*plan** is a plan of upgrade.", - "allOf": [ - { - "$ref": "#/definitions/Plan" - } - ] - }, - "title": { - "description": "*title** is a text title of proposal.", - "type": "string" - }, - "upgraded_client_state": { - "description": "*upgraded_client_state** is an upgraded client state.", - "allOf": [ - { - "$ref": "#/definitions/ProtobufAny" - } - ] - } - } - }, "VoteOption": { "type": "string", "enum": [ diff --git a/schemas/astroport-xastro-token/astroport-xastro-token.json b/schemas/astroport-xastro-token/astroport-xastro-token.json deleted file mode 100644 index e4d53a771..000000000 --- a/schemas/astroport-xastro-token/astroport-xastro-token.json +++ /dev/null @@ -1,1340 +0,0 @@ -{ - "contract_name": "astroport-xastro-token", - "contract_version": "1.1.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a xASTRO token contract.", - "type": "object", - "required": [ - "decimals", - "initial_balances", - "name", - "symbol" - ], - "properties": { - "decimals": { - "description": "The number of decimals the token has", - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "initial_balances": { - "description": "Initial token balances", - "type": "array", - "items": { - "$ref": "#/definitions/Cw20Coin" - } - }, - "marketing": { - "description": "the marketing info of type [`InstantiateMarketingInfo`]", - "anyOf": [ - { - "$ref": "#/definitions/InstantiateMarketingInfo" - }, - { - "type": "null" - } - ] - }, - "mint": { - "description": "Token minting permissions", - "anyOf": [ - { - "$ref": "#/definitions/MinterResponse" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": "Token name", - "type": "string" - }, - "symbol": { - "description": "Token symbol", - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20Coin": { - "type": "object", - "required": [ - "address", - "amount" - ], - "properties": { - "address": { - "type": "string" - }, - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - }, - "EmbeddedLogo": { - "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", - "oneOf": [ - { - "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", - "type": "object", - "required": [ - "svg" - ], - "properties": { - "svg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - }, - { - "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", - "type": "object", - "required": [ - "png" - ], - "properties": { - "png": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - ] - }, - "InstantiateMarketingInfo": { - "description": "This structure describes the marketing info settings such as project, description, and token logo.", - "type": "object", - "properties": { - "description": { - "description": "The project description", - "type": [ - "string", - "null" - ] - }, - "logo": { - "description": "The token logo", - "anyOf": [ - { - "$ref": "#/definitions/Logo" - }, - { - "type": "null" - } - ] - }, - "marketing": { - "description": "The address of an admin who is able to update marketing info", - "type": [ - "string", - "null" - ] - }, - "project": { - "description": "The project name", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "Logo": { - "description": "This is used for uploading logo data, or setting it in InstantiateData", - "oneOf": [ - { - "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", - "type": "object", - "required": [ - "embedded" - ], - "properties": { - "embedded": { - "$ref": "#/definitions/EmbeddedLogo" - } - }, - "additionalProperties": false - } - ] - }, - "MinterResponse": { - "type": "object", - "required": [ - "minter" - ], - "properties": { - "cap": { - "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "minter": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "oneOf": [ - { - "description": "Transfer is a base message to move tokens to another account without triggering actions", - "type": "object", - "required": [ - "transfer" - ], - "properties": { - "transfer": { - "type": "object", - "required": [ - "amount", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "recipient": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Burn is a base message to destroy tokens forever", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Send is a base message to transfer tokens to a contract and trigger an action on the receiving contract.", - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "contract", - "msg" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "contract": { - "type": "string" - }, - "msg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Allows spender to access an additional amount tokens from the owner's (env.sender) account. If expires is Some(), overwrites current allowance expiration with this one.", - "type": "object", - "required": [ - "increase_allowance" - ], - "properties": { - "increase_allowance": { - "type": "object", - "required": [ - "amount", - "spender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Lowers the spender's access of tokens from the owner's (env.sender) account by amount. If expires is Some(), overwrites current allowance expiration with this one.", - "type": "object", - "required": [ - "decrease_allowance" - ], - "properties": { - "decrease_allowance": { - "type": "object", - "required": [ - "amount", - "spender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Transfers amount tokens from owner -> recipient if `env.sender` has sufficient pre-approval.", - "type": "object", - "required": [ - "transfer_from" - ], - "properties": { - "transfer_from": { - "type": "object", - "required": [ - "amount", - "owner", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "owner": { - "type": "string" - }, - "recipient": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Sends amount tokens from owner -> contract if `env.sender` has sufficient pre-approval.", - "type": "object", - "required": [ - "send_from" - ], - "properties": { - "send_from": { - "type": "object", - "required": [ - "amount", - "contract", - "msg", - "owner" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "contract": { - "type": "string" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Destroys tokens forever", - "type": "object", - "required": [ - "burn_from" - ], - "properties": { - "burn_from": { - "type": "object", - "required": [ - "amount", - "owner" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with the \"mintable\" extension. If authorized, creates amount new tokens and adds to the recipient balance.", - "type": "object", - "required": [ - "mint" - ], - "properties": { - "mint": { - "type": "object", - "required": [ - "amount", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "recipient": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with the \"mintable\" extension. The current minter may set a new minter. Setting the minter to None will remove the token's minter forever.", - "type": "object", - "required": [ - "update_minter" - ], - "properties": { - "update_minter": { - "type": "object", - "properties": { - "new_minter": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with the \"marketing\" extension. If authorized, updates marketing metadata. Setting None/null for any of these will leave it unchanged. Setting Some(\"\") will clear this field on the contract storage", - "type": "object", - "required": [ - "update_marketing" - ], - "properties": { - "update_marketing": { - "type": "object", - "properties": { - "description": { - "description": "A longer description of the token and it's utility. Designed for tooltips or such", - "type": [ - "string", - "null" - ] - }, - "marketing": { - "description": "The address (if any) who can update this data structure", - "type": [ - "string", - "null" - ] - }, - "project": { - "description": "A URL pointing to the project behind this token.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "If set as the \"marketing\" role on the contract, upload a new URL, SVG, or PNG for the token", - "type": "object", - "required": [ - "upload_logo" - ], - "properties": { - "upload_logo": { - "$ref": "#/definitions/Logo" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "EmbeddedLogo": { - "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", - "oneOf": [ - { - "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", - "type": "object", - "required": [ - "svg" - ], - "properties": { - "svg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - }, - { - "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", - "type": "object", - "required": [ - "png" - ], - "properties": { - "png": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - ] - }, - "Expiration": { - "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", - "oneOf": [ - { - "description": "AtHeight will expire when `env.block.height` >= height", - "type": "object", - "required": [ - "at_height" - ], - "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "description": "AtTime will expire when `env.block.time` >= time", - "type": "object", - "required": [ - "at_time" - ], - "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - }, - { - "description": "Never will never expire. Used to express the empty variant", - "type": "object", - "required": [ - "never" - ], - "properties": { - "never": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Logo": { - "description": "This is used for uploading logo data, or setting it in InstantiateData", - "oneOf": [ - { - "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", - "type": "object", - "required": [ - "embedded" - ], - "properties": { - "embedded": { - "$ref": "#/definitions/EmbeddedLogo" - } - }, - "additionalProperties": false - } - ] - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This enum describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Balance returns the current balance of a given address, 0 if unset.", - "type": "object", - "required": [ - "balance" - ], - "properties": { - "balance": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "BalanceAt returns balance of the given address at the given block, 0 if unset.", - "type": "object", - "required": [ - "balance_at" - ], - "properties": { - "balance_at": { - "type": "object", - "required": [ - "address", - "block" - ], - "properties": { - "address": { - "type": "string" - }, - "block": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "TotalSupplyAt returns the total token supply at the given block.", - "type": "object", - "required": [ - "total_supply_at" - ], - "properties": { - "total_supply_at": { - "type": "object", - "required": [ - "block" - ], - "properties": { - "block": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "TokenInfo returns the contract's metadata - name, decimals, supply, etc.", - "type": "object", - "required": [ - "token_info" - ], - "properties": { - "token_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns who can mint xASTRO and the hard cap on maximum tokens after minting.", - "type": "object", - "required": [ - "minter" - ], - "properties": { - "minter": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Allowance returns an amount of tokens the spender can spend from the owner account, 0 if unset.", - "type": "object", - "required": [ - "allowance" - ], - "properties": { - "allowance": { - "type": "object", - "required": [ - "owner", - "spender" - ], - "properties": { - "owner": { - "type": "string" - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "AllAllowances returns all the allowances this token holder has approved. Supports pagination.", - "type": "object", - "required": [ - "all_allowances" - ], - "properties": { - "all_allowances": { - "type": "object", - "required": [ - "owner" - ], - "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "type": "string" - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "AllAccounts returns all the accounts that have xASTRO balances. Supports pagination.", - "type": "object", - "required": [ - "all_accounts" - ], - "properties": { - "all_accounts": { - "type": "object", - "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns marketing related contract metadata: - description, logo, project url, etc.", - "type": "object", - "required": [ - "marketing_info" - ], - "properties": { - "marketing_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Downloads embeded logo data (if stored on chain). Errors if no logo data was stored for this contract.", - "type": "object", - "required": [ - "download_logo" - ], - "properties": { - "download_logo": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "migrate": null, - "sudo": null, - "responses": { - "all_accounts": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllAccountsResponse", - "type": "object", - "required": [ - "accounts" - ], - "properties": { - "accounts": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "all_allowances": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllAllowancesResponse", - "type": "object", - "required": [ - "allowances" - ], - "properties": { - "allowances": { - "type": "array", - "items": { - "$ref": "#/definitions/AllowanceInfo" - } - } - }, - "definitions": { - "AllowanceInfo": { - "type": "object", - "required": [ - "allowance", - "expires", - "spender" - ], - "properties": { - "allowance": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "$ref": "#/definitions/Expiration" - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Expiration": { - "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", - "oneOf": [ - { - "description": "AtHeight will expire when `env.block.height` >= height", - "type": "object", - "required": [ - "at_height" - ], - "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "description": "AtTime will expire when `env.block.time` >= time", - "type": "object", - "required": [ - "at_time" - ], - "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - }, - { - "description": "Never will never expire. Used to express the empty variant", - "type": "object", - "required": [ - "never" - ], - "properties": { - "never": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "allowance": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllowanceResponse", - "type": "object", - "required": [ - "allowance", - "expires" - ], - "properties": { - "allowance": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "$ref": "#/definitions/Expiration" - } - }, - "definitions": { - "Expiration": { - "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", - "oneOf": [ - { - "description": "AtHeight will expire when `env.block.height` >= height", - "type": "object", - "required": [ - "at_height" - ], - "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "description": "AtTime will expire when `env.block.time` >= time", - "type": "object", - "required": [ - "at_time" - ], - "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - }, - { - "description": "Never will never expire. Used to express the empty variant", - "type": "object", - "required": [ - "never" - ], - "properties": { - "never": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } - }, - "balance": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalanceResponse", - "type": "object", - "required": [ - "balance" - ], - "properties": { - "balance": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "balance_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalanceResponse", - "type": "object", - "required": [ - "balance" - ], - "properties": { - "balance": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "download_logo": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "DownloadLogoResponse", - "description": "When we download an embedded logo, we get this response type. We expect a SPA to be able to accept this info and display it.", - "type": "object", - "required": [ - "data", - "mime_type" - ], - "properties": { - "data": { - "$ref": "#/definitions/Binary" - }, - "mime_type": { - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } - }, - "marketing_info": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MarketingInfoResponse", - "type": "object", - "properties": { - "description": { - "description": "A longer description of the token and it's utility. Designed for tooltips or such", - "type": [ - "string", - "null" - ] - }, - "logo": { - "description": "A link to the logo, or a comment there is an on-chain logo stored", - "anyOf": [ - { - "$ref": "#/definitions/LogoInfo" - }, - { - "type": "null" - } - ] - }, - "marketing": { - "description": "The address (if any) who can update this data structure", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "project": { - "description": "A URL pointing to the project behind this token.", - "type": [ - "string", - "null" - ] - } - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "LogoInfo": { - "description": "This is used to display logo info, provide a link or inform there is one that can be downloaded from the blockchain itself", - "oneOf": [ - { - "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "There is an embedded logo on the chain, make another call to download it.", - "type": "string", - "enum": [ - "embedded" - ] - } - ] - } - } - }, - "minter": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_MinterResponse", - "anyOf": [ - { - "$ref": "#/definitions/MinterResponse" - }, - { - "type": "null" - } - ], - "definitions": { - "MinterResponse": { - "type": "object", - "required": [ - "minter" - ], - "properties": { - "cap": { - "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "minter": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "token_info": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "TokenInfoResponse", - "type": "object", - "required": [ - "decimals", - "name", - "symbol", - "total_supply" - ], - "properties": { - "decimals": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "name": { - "type": "string" - }, - "symbol": { - "type": "string" - }, - "total_supply": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "total_supply_at": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/execute.json b/schemas/astroport-xastro-token/raw/execute.json deleted file mode 100644 index 9f0b97868..000000000 --- a/schemas/astroport-xastro-token/raw/execute.json +++ /dev/null @@ -1,475 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "oneOf": [ - { - "description": "Transfer is a base message to move tokens to another account without triggering actions", - "type": "object", - "required": [ - "transfer" - ], - "properties": { - "transfer": { - "type": "object", - "required": [ - "amount", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "recipient": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Burn is a base message to destroy tokens forever", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Send is a base message to transfer tokens to a contract and trigger an action on the receiving contract.", - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "contract", - "msg" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "contract": { - "type": "string" - }, - "msg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Allows spender to access an additional amount tokens from the owner's (env.sender) account. If expires is Some(), overwrites current allowance expiration with this one.", - "type": "object", - "required": [ - "increase_allowance" - ], - "properties": { - "increase_allowance": { - "type": "object", - "required": [ - "amount", - "spender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Lowers the spender's access of tokens from the owner's (env.sender) account by amount. If expires is Some(), overwrites current allowance expiration with this one.", - "type": "object", - "required": [ - "decrease_allowance" - ], - "properties": { - "decrease_allowance": { - "type": "object", - "required": [ - "amount", - "spender" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Transfers amount tokens from owner -> recipient if `env.sender` has sufficient pre-approval.", - "type": "object", - "required": [ - "transfer_from" - ], - "properties": { - "transfer_from": { - "type": "object", - "required": [ - "amount", - "owner", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "owner": { - "type": "string" - }, - "recipient": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Sends amount tokens from owner -> contract if `env.sender` has sufficient pre-approval.", - "type": "object", - "required": [ - "send_from" - ], - "properties": { - "send_from": { - "type": "object", - "required": [ - "amount", - "contract", - "msg", - "owner" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "contract": { - "type": "string" - }, - "msg": { - "$ref": "#/definitions/Binary" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with \"approval\" extension. Destroys tokens forever", - "type": "object", - "required": [ - "burn_from" - ], - "properties": { - "burn_from": { - "type": "object", - "required": [ - "amount", - "owner" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with the \"mintable\" extension. If authorized, creates amount new tokens and adds to the recipient balance.", - "type": "object", - "required": [ - "mint" - ], - "properties": { - "mint": { - "type": "object", - "required": [ - "amount", - "recipient" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "recipient": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with the \"mintable\" extension. The current minter may set a new minter. Setting the minter to None will remove the token's minter forever.", - "type": "object", - "required": [ - "update_minter" - ], - "properties": { - "update_minter": { - "type": "object", - "properties": { - "new_minter": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only with the \"marketing\" extension. If authorized, updates marketing metadata. Setting None/null for any of these will leave it unchanged. Setting Some(\"\") will clear this field on the contract storage", - "type": "object", - "required": [ - "update_marketing" - ], - "properties": { - "update_marketing": { - "type": "object", - "properties": { - "description": { - "description": "A longer description of the token and it's utility. Designed for tooltips or such", - "type": [ - "string", - "null" - ] - }, - "marketing": { - "description": "The address (if any) who can update this data structure", - "type": [ - "string", - "null" - ] - }, - "project": { - "description": "A URL pointing to the project behind this token.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "If set as the \"marketing\" role on the contract, upload a new URL, SVG, or PNG for the token", - "type": "object", - "required": [ - "upload_logo" - ], - "properties": { - "upload_logo": { - "$ref": "#/definitions/Logo" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "EmbeddedLogo": { - "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", - "oneOf": [ - { - "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", - "type": "object", - "required": [ - "svg" - ], - "properties": { - "svg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - }, - { - "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", - "type": "object", - "required": [ - "png" - ], - "properties": { - "png": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - ] - }, - "Expiration": { - "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", - "oneOf": [ - { - "description": "AtHeight will expire when `env.block.height` >= height", - "type": "object", - "required": [ - "at_height" - ], - "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "description": "AtTime will expire when `env.block.time` >= time", - "type": "object", - "required": [ - "at_time" - ], - "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - }, - { - "description": "Never will never expire. Used to express the empty variant", - "type": "object", - "required": [ - "never" - ], - "properties": { - "never": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Logo": { - "description": "This is used for uploading logo data, or setting it in InstantiateData", - "oneOf": [ - { - "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", - "type": "object", - "required": [ - "embedded" - ], - "properties": { - "embedded": { - "$ref": "#/definitions/EmbeddedLogo" - } - }, - "additionalProperties": false - } - ] - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/instantiate.json b/schemas/astroport-xastro-token/raw/instantiate.json deleted file mode 100644 index 7a62784e0..000000000 --- a/schemas/astroport-xastro-token/raw/instantiate.json +++ /dev/null @@ -1,208 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "description": "This structure describes the parameters used for creating a xASTRO token contract.", - "type": "object", - "required": [ - "decimals", - "initial_balances", - "name", - "symbol" - ], - "properties": { - "decimals": { - "description": "The number of decimals the token has", - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "initial_balances": { - "description": "Initial token balances", - "type": "array", - "items": { - "$ref": "#/definitions/Cw20Coin" - } - }, - "marketing": { - "description": "the marketing info of type [`InstantiateMarketingInfo`]", - "anyOf": [ - { - "$ref": "#/definitions/InstantiateMarketingInfo" - }, - { - "type": "null" - } - ] - }, - "mint": { - "description": "Token minting permissions", - "anyOf": [ - { - "$ref": "#/definitions/MinterResponse" - }, - { - "type": "null" - } - ] - }, - "name": { - "description": "Token name", - "type": "string" - }, - "symbol": { - "description": "Token symbol", - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Cw20Coin": { - "type": "object", - "required": [ - "address", - "amount" - ], - "properties": { - "address": { - "type": "string" - }, - "amount": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - }, - "EmbeddedLogo": { - "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.", - "oneOf": [ - { - "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)", - "type": "object", - "required": [ - "svg" - ], - "properties": { - "svg": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - }, - { - "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.", - "type": "object", - "required": [ - "png" - ], - "properties": { - "png": { - "$ref": "#/definitions/Binary" - } - }, - "additionalProperties": false - } - ] - }, - "InstantiateMarketingInfo": { - "description": "This structure describes the marketing info settings such as project, description, and token logo.", - "type": "object", - "properties": { - "description": { - "description": "The project description", - "type": [ - "string", - "null" - ] - }, - "logo": { - "description": "The token logo", - "anyOf": [ - { - "$ref": "#/definitions/Logo" - }, - { - "type": "null" - } - ] - }, - "marketing": { - "description": "The address of an admin who is able to update marketing info", - "type": [ - "string", - "null" - ] - }, - "project": { - "description": "The project name", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "Logo": { - "description": "This is used for uploading logo data, or setting it in InstantiateData", - "oneOf": [ - { - "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants", - "type": "object", - "required": [ - "embedded" - ], - "properties": { - "embedded": { - "$ref": "#/definitions/EmbeddedLogo" - } - }, - "additionalProperties": false - } - ] - }, - "MinterResponse": { - "type": "object", - "required": [ - "minter" - ], - "properties": { - "cap": { - "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "minter": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/query.json b/schemas/astroport-xastro-token/raw/query.json deleted file mode 100644 index 92634f70d..000000000 --- a/schemas/astroport-xastro-token/raw/query.json +++ /dev/null @@ -1,229 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "description": "This enum describes the query messages available in the contract.", - "oneOf": [ - { - "description": "Balance returns the current balance of a given address, 0 if unset.", - "type": "object", - "required": [ - "balance" - ], - "properties": { - "balance": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "BalanceAt returns balance of the given address at the given block, 0 if unset.", - "type": "object", - "required": [ - "balance_at" - ], - "properties": { - "balance_at": { - "type": "object", - "required": [ - "address", - "block" - ], - "properties": { - "address": { - "type": "string" - }, - "block": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "TotalSupplyAt returns the total token supply at the given block.", - "type": "object", - "required": [ - "total_supply_at" - ], - "properties": { - "total_supply_at": { - "type": "object", - "required": [ - "block" - ], - "properties": { - "block": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "TokenInfo returns the contract's metadata - name, decimals, supply, etc.", - "type": "object", - "required": [ - "token_info" - ], - "properties": { - "token_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns who can mint xASTRO and the hard cap on maximum tokens after minting.", - "type": "object", - "required": [ - "minter" - ], - "properties": { - "minter": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Allowance returns an amount of tokens the spender can spend from the owner account, 0 if unset.", - "type": "object", - "required": [ - "allowance" - ], - "properties": { - "allowance": { - "type": "object", - "required": [ - "owner", - "spender" - ], - "properties": { - "owner": { - "type": "string" - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "AllAllowances returns all the allowances this token holder has approved. Supports pagination.", - "type": "object", - "required": [ - "all_allowances" - ], - "properties": { - "all_allowances": { - "type": "object", - "required": [ - "owner" - ], - "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "type": "string" - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "AllAccounts returns all the accounts that have xASTRO balances. Supports pagination.", - "type": "object", - "required": [ - "all_accounts" - ], - "properties": { - "all_accounts": { - "type": "object", - "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns marketing related contract metadata: - description, logo, project url, etc.", - "type": "object", - "required": [ - "marketing_info" - ], - "properties": { - "marketing_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Downloads embeded logo data (if stored on chain). Errors if no logo data was stored for this contract.", - "type": "object", - "required": [ - "download_logo" - ], - "properties": { - "download_logo": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] -} diff --git a/schemas/astroport-xastro-token/raw/response_to_all_accounts.json b/schemas/astroport-xastro-token/raw/response_to_all_accounts.json deleted file mode 100644 index cea50fba4..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_all_accounts.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllAccountsResponse", - "type": "object", - "required": [ - "accounts" - ], - "properties": { - "accounts": { - "type": "array", - "items": { - "type": "string" - } - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_all_allowances.json b/schemas/astroport-xastro-token/raw/response_to_all_allowances.json deleted file mode 100644 index 012872250..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_all_allowances.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllAllowancesResponse", - "type": "object", - "required": [ - "allowances" - ], - "properties": { - "allowances": { - "type": "array", - "items": { - "$ref": "#/definitions/AllowanceInfo" - } - } - }, - "definitions": { - "AllowanceInfo": { - "type": "object", - "required": [ - "allowance", - "expires", - "spender" - ], - "properties": { - "allowance": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "$ref": "#/definitions/Expiration" - }, - "spender": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Expiration": { - "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", - "oneOf": [ - { - "description": "AtHeight will expire when `env.block.height` >= height", - "type": "object", - "required": [ - "at_height" - ], - "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "description": "AtTime will expire when `env.block.time` >= time", - "type": "object", - "required": [ - "at_time" - ], - "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - }, - { - "description": "Never will never expire. Used to express the empty variant", - "type": "object", - "required": [ - "never" - ], - "properties": { - "never": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_allowance.json b/schemas/astroport-xastro-token/raw/response_to_allowance.json deleted file mode 100644 index dbaf97db7..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_allowance.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllowanceResponse", - "type": "object", - "required": [ - "allowance", - "expires" - ], - "properties": { - "allowance": { - "$ref": "#/definitions/Uint128" - }, - "expires": { - "$ref": "#/definitions/Expiration" - } - }, - "definitions": { - "Expiration": { - "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", - "oneOf": [ - { - "description": "AtHeight will expire when `env.block.height` >= height", - "type": "object", - "required": [ - "at_height" - ], - "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - { - "description": "AtTime will expire when `env.block.time` >= time", - "type": "object", - "required": [ - "at_time" - ], - "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - }, - { - "description": "Never will never expire. Used to express the empty variant", - "type": "object", - "required": [ - "never" - ], - "properties": { - "never": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Timestamp": { - "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", - "allOf": [ - { - "$ref": "#/definitions/Uint64" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "Uint64": { - "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_balance.json b/schemas/astroport-xastro-token/raw/response_to_balance.json deleted file mode 100644 index 7dcf4d4a5..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_balance.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalanceResponse", - "type": "object", - "required": [ - "balance" - ], - "properties": { - "balance": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_balance_at.json b/schemas/astroport-xastro-token/raw/response_to_balance_at.json deleted file mode 100644 index 7dcf4d4a5..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_balance_at.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BalanceResponse", - "type": "object", - "required": [ - "balance" - ], - "properties": { - "balance": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_download_logo.json b/schemas/astroport-xastro-token/raw/response_to_download_logo.json deleted file mode 100644 index c5aa32b92..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_download_logo.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "DownloadLogoResponse", - "description": "When we download an embedded logo, we get this response type. We expect a SPA to be able to accept this info and display it.", - "type": "object", - "required": [ - "data", - "mime_type" - ], - "properties": { - "data": { - "$ref": "#/definitions/Binary" - }, - "mime_type": { - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_marketing_info.json b/schemas/astroport-xastro-token/raw/response_to_marketing_info.json deleted file mode 100644 index c36ee5f9c..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_marketing_info.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MarketingInfoResponse", - "type": "object", - "properties": { - "description": { - "description": "A longer description of the token and it's utility. Designed for tooltips or such", - "type": [ - "string", - "null" - ] - }, - "logo": { - "description": "A link to the logo, or a comment there is an on-chain logo stored", - "anyOf": [ - { - "$ref": "#/definitions/LogoInfo" - }, - { - "type": "null" - } - ] - }, - "marketing": { - "description": "The address (if any) who can update this data structure", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "project": { - "description": "A URL pointing to the project behind this token.", - "type": [ - "string", - "null" - ] - } - }, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "LogoInfo": { - "description": "This is used to display logo info, provide a link or inform there is one that can be downloaded from the blockchain itself", - "oneOf": [ - { - "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.", - "type": "object", - "required": [ - "url" - ], - "properties": { - "url": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "There is an embedded logo on the chain, make another call to download it.", - "type": "string", - "enum": [ - "embedded" - ] - } - ] - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_minter.json b/schemas/astroport-xastro-token/raw/response_to_minter.json deleted file mode 100644 index 1294efd78..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_minter.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_MinterResponse", - "anyOf": [ - { - "$ref": "#/definitions/MinterResponse" - }, - { - "type": "null" - } - ], - "definitions": { - "MinterResponse": { - "type": "object", - "required": [ - "minter" - ], - "properties": { - "cap": { - "description": "cap is a hard cap on total supply that can be achieved by minting. Note that this refers to total_supply. If None, there is unlimited cap.", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "minter": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_token_info.json b/schemas/astroport-xastro-token/raw/response_to_token_info.json deleted file mode 100644 index 0e84d125f..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_token_info.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "TokenInfoResponse", - "type": "object", - "required": [ - "decimals", - "name", - "symbol", - "total_supply" - ], - "properties": { - "decimals": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "name": { - "type": "string" - }, - "symbol": { - "type": "string" - }, - "total_supply": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false, - "definitions": { - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } -} diff --git a/schemas/astroport-xastro-token/raw/response_to_total_supply_at.json b/schemas/astroport-xastro-token/raw/response_to_total_supply_at.json deleted file mode 100644 index 25b73e8f2..000000000 --- a/schemas/astroport-xastro-token/raw/response_to_total_supply_at.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Uint128", - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" -} diff --git a/scripts/build_juno_v1_deployment_command.py b/scripts/build_juno_v1_deployment_command.py new file mode 100644 index 000000000..95342a3d0 --- /dev/null +++ b/scripts/build_juno_v1_deployment_command.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Build/render the final Astroport-Juno v1 deployment fill command. + +This glues together the two handoff streams: + +1. `scripts/extract_juno_v1_tx_sets.py` output from real `junod -o json` + store/instantiate transactions. +2. Manual operator values that do not appear in tx logs: owner/guardian/treasury, + tokenfactory module account, and the first counterparty denom for the sample + XYK pair create message. + +By default it prints a copy/paste-safe command for +`scripts/fill_juno_v1_deployment_config.py`. With `--render`, it also executes +that command and validates the rendered config with the deployment-template +schema/scope guard. +""" +from __future__ import annotations + +import argparse +import pathlib +import shlex +import subprocess +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +FILL = ROOT / "scripts" / "fill_juno_v1_deployment_config.py" +CHECK_TEMPLATE = ROOT / "scripts" / "check_juno_v1_deployment_template.py" + +REQUIRED_TX_SET_PATHS = { + "code_ids.astroport-factory", + "code_ids.astroport-incentives", + "code_ids.astroport-native-coin-registry", + "code_ids.astroport-oracle", + "code_ids.astroport-pair", + "code_ids.astroport-router", + "code_ids.astroport-tokenfactory-tracker", + "code_ids.astroport-whitelist", + "code_ids.cw20-base", + "addresses.astroport-factory", + "addresses.astroport-incentives", + "addresses.astroport-native-coin-registry", + "addresses.astroport-oracle", + "addresses.astroport-router", + "addresses.astroport-tokenfactory-tracker", + "addresses.astroport-whitelist", +} + +MANUAL_SET_PATHS = { + "accounts.owner", + "accounts.guardian", + "accounts.treasury", + "accounts.tokenfactory_module", + "pair_create_msg_template.asset_infos.1.native_token.denom", +} + +NETWORKS = { + "uni-7": { + "network.chain_id": "uni-7", + "network.fee_denom": "ujunox", + "network.native_asset_denom": "ujunox", + }, + "juno-1": { + "network.chain_id": "juno-1", + "network.fee_denom": "ujuno", + "network.native_asset_denom": "ujuno", + }, +} +NETWORK_SET_PATHS = set(next(iter(NETWORKS.values()))) + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def parse_set_assignment(raw: str) -> tuple[str, str]: + if "=" not in raw: + fail(f"--set assignment must be dotted.path=value, got {raw!r}") + path, value = raw.split("=", 1) + if not path or not value: + fail(f"--set assignment must include non-empty path and value, got {raw!r}") + return path, value + + +def parse_extractor_sets(path: pathlib.Path) -> list[str]: + try: + lines = path.read_text().splitlines() + except FileNotFoundError: + fail(f"missing tx sets file: {path}") + + assignments: list[str] = [] + for line_no, line in enumerate(lines, start=1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + parts = shlex.split(stripped) + if len(parts) != 2 or parts[0] != "--set": + fail(f"{path}:{line_no} expected extractor line like `--set path=value`, got {line!r}") + assignments.append(parts[1]) + if not assignments: + fail(f"no --set assignments found in {path}") + return assignments + + +def add_assignment(assignments: dict[str, str], assignment: str, source: str) -> None: + path, value = parse_set_assignment(assignment) + previous = assignments.get(path) + if previous is not None and previous != value: + fail(f"conflicting values for {path}: {previous!r} vs {value!r} from {source}") + assignments[path] = value + + +def shell_command(assignments: dict[str, str], output: pathlib.Path) -> str: + args = [ + "python3", + "scripts/fill_juno_v1_deployment_config.py", + "--output", + str(output), + "--require-complete", + ] + for path in sorted(assignments): + args.extend(["--set", f"{path}={assignments[path]}"]) + return " \\\n ".join(shlex.quote(arg) for arg in args) + + +def render(assignments: dict[str, str], output: pathlib.Path) -> None: + args = [sys.executable, str(FILL), "--output", str(output), "--require-complete"] + for path in sorted(assignments): + args.extend(["--set", f"{path}={assignments[path]}"]) + fill_proc = subprocess.run(args, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if fill_proc.returncode != 0: + fail(f"fill command failed:\nstdout={fill_proc.stdout}\nstderr={fill_proc.stderr}") + print(fill_proc.stdout, end="") + + check_proc = subprocess.run( + [sys.executable, str(CHECK_TEMPLATE), str(output)], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if check_proc.returncode != 0: + fail(f"rendered config failed template guard:\nstdout={check_proc.stdout}\nstderr={check_proc.stderr}") + print(check_proc.stdout, end="") + + +def reject_unsafe_mainnet_output(network: str, output: pathlib.Path) -> None: + if "mainnet" in output.name and network != "juno-1": + fail("mainnet deployment output requires --network juno-1") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--tx-sets", type=pathlib.Path, required=True, help="file containing extractor `--set ...` lines") + parser.add_argument("--owner", required=True) + parser.add_argument("--guardian", required=True) + parser.add_argument("--treasury", required=True) + parser.add_argument("--tokenfactory-module", required=True) + parser.add_argument("--counterparty-denom", required=True, help="ibc/... denom for the sample JUNO/counterparty XYK pair-create template") + parser.add_argument("--output", type=pathlib.Path, default=pathlib.Path("deployment/juno-v1-testnet.filled.json")) + parser.add_argument("--network", choices=sorted(NETWORKS), default="uni-7", help="deployment network values to render into the config") + parser.add_argument("--render", action="store_true", help="execute the fill command and validate the rendered config") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + assignments: dict[str, str] = {} + reject_unsafe_mainnet_output(args.network, args.output) + + for assignment in parse_extractor_sets(args.tx_sets): + add_assignment(assignments, assignment, str(args.tx_sets)) + + manual = { + "accounts.owner": args.owner, + "accounts.guardian": args.guardian, + "accounts.treasury": args.treasury, + "accounts.tokenfactory_module": args.tokenfactory_module, + "pair_create_msg_template.asset_infos.1.native_token.denom": args.counterparty_denom, + } + for path, value in manual.items(): + add_assignment(assignments, f"{path}={value}", "manual arg") + for path, value in NETWORKS[args.network].items(): + add_assignment(assignments, f"{path}={value}", f"network {args.network}") + + missing_tx = sorted(REQUIRED_TX_SET_PATHS - assignments.keys()) + if missing_tx: + fail("tx sets missing required deployment values: " + ", ".join(missing_tx)) + extra_tx = sorted(path for path in assignments if path not in REQUIRED_TX_SET_PATHS | MANUAL_SET_PATHS | NETWORK_SET_PATHS) + if extra_tx: + fail("unexpected deployment set paths: " + ", ".join(extra_tx)) + + command = shell_command(assignments, args.output) + print(command) + print(f"sets={len(assignments)} tx_sets={len(REQUIRED_TX_SET_PATHS)} manual_sets={len(MANUAL_SET_PATHS)} network={args.network} render={args.render}") + + if args.render: + render(assignments, args.output) + + +if __name__ == "__main__": + main() diff --git a/scripts/build_juno_v1_frontend_release_bundle.py b/scripts/build_juno_v1_frontend_release_bundle.py new file mode 100644 index 000000000..572e3d238 --- /dev/null +++ b/scripts/build_juno_v1_frontend_release_bundle.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Package the verified Astroport-Juno v1 frontend handoff files. + +The bundle is intentionally narrow: a rendered uni-7 deployment config, the +Juno v1 TypeScript declaration, the optional example fixture, and a manifest +with sha256/size metadata. It does not include the placeholder template, tx +logs, wasm artifacts, or any deferred DEX surfaces. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import subprocess +import sys +import zipfile +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DEFAULT_CONFIG = ROOT / "deployment" / "juno-v1-testnet.json" +DEFAULT_TYPES = ROOT / "deployment" / "juno-v1-frontend-config.d.ts" +DEFAULT_EXAMPLE = ROOT / "deployment" / "juno-v1-frontend-config.example.ts" +DEFAULT_OUTPUT = ROOT / "deployment" / "juno-v1-frontend-release.zip" +CHECK_TEMPLATE = ROOT / "scripts" / "check_juno_v1_deployment_template.py" +CHECK_FRONTEND = ROOT / "scripts" / "check_juno_v1_frontend_config.py" +GENERATE_TYPES = ROOT / "scripts" / "generate_juno_v1_frontend_types.py" +CHECK_EXAMPLE = ROOT / "scripts" / "check_juno_v1_frontend_example.py" +CHECK_SYNC = ROOT / "scripts" / "check_juno_v1_frontend_handoff_sync.py" + +BUNDLE_CONFIG_NAME = "juno-v1-testnet.json" +BUNDLE_TYPES_NAME = "juno-v1-frontend-config.d.ts" +BUNDLE_EXAMPLE_NAME = "juno-v1-frontend-config.example.ts" +BUNDLE_MANIFEST_NAME = "MANIFEST.json" +FORBIDDEN_NAMES = { + "juno-v1-testnet.template.json", + "tx-sets.txt", +} +FORBIDDEN_SUBSTRINGS = ( + "stable", + "concentrated", + "pcl", + "xastro", + "vesting", + "maker", + "perps", + "lst", +) +FORBIDDEN_CONTENT_SUBSTRINGS = tuple( + fragment for fragment in FORBIDDEN_SUBSTRINGS if fragment not in {"maker", "vesting"} +) + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def repo_relative(path: pathlib.Path) -> str: + try: + return str(path.resolve().relative_to(ROOT)) + except ValueError: + return str(path) + + +def run(args: list[str]) -> str: + proc = subprocess.run(args, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if proc.returncode != 0: + fail(f"command failed: {' '.join(args)}\nstdout={proc.stdout}\nstderr={proc.stderr}") + return proc.stdout + + +def read_bytes(path: pathlib.Path) -> bytes: + try: + return path.read_bytes() + except FileNotFoundError: + fail(f"missing release input: {repo_relative(path)}") + + +def load_config(path: pathlib.Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing release config: {repo_relative(path)}") + except json.JSONDecodeError as exc: + fail(f"invalid release config JSON: {exc}") + if not isinstance(data, dict): + fail("release config must be a JSON object") + return data + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def assert_not_template(config: dict[str, Any]) -> None: + text = json.dumps(config, sort_keys=True).lower() + for marker in ("todo", "placeholder", "replace-me"): + if marker in text: + fail(f"release config still contains placeholder marker: {marker}") + + +def assert_bundle_scope(entries: dict[str, bytes]) -> None: + for name, data in entries.items(): + lower_name = name.lower() + if name in FORBIDDEN_NAMES or lower_name.startswith("tx/") or lower_name.endswith(".wasm"): + fail(f"forbidden file in frontend bundle: {name}") + if any(fragment in lower_name for fragment in FORBIDDEN_SUBSTRINGS): + fail(f"forbidden deferred-scope filename in frontend bundle: {name}") + if name.endswith((".json", ".ts")): + text = data.decode("utf-8", errors="ignore").lower() + bad = [fragment for fragment in FORBIDDEN_CONTENT_SUBSTRINGS if fragment in text] + # The v1 config still carries schema-required maker_fee_bps, but + # should not mention any deferred pool/DEX-token surfaces in + # release bundle contents. + if bad: + fail(f"forbidden deferred-scope text in {name}: {', '.join(sorted(set(bad)))}") + + +def build_manifest(entries: dict[str, bytes], config: dict[str, Any]) -> dict[str, Any]: + frontend = config.get("frontend", {}) + network = config.get("network", {}) + addresses = config.get("addresses", {}) + return { + "bundle": "astroport-juno-v1-frontend-release", + "network": network.get("chain_id"), + "native_asset_denom": network.get("native_asset_denom"), + "scope": "xyk-only, permissionless, no DEX token", + "pair_discovery": frontend.get("pair_discovery"), + "required_frontend_addresses": frontend.get("required_addresses", []), + "optional_frontend_addresses": frontend.get("optional_addresses", []), + "address_count": len(addresses) if isinstance(addresses, dict) else None, + "files": [ + { + "path": name, + "bytes": len(data), + "sha256": sha256(data), + } + for name, data in entries.items() + ], + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--config", type=pathlib.Path, default=DEFAULT_CONFIG, help="rendered deployment config JSON") + parser.add_argument("--types", type=pathlib.Path, default=DEFAULT_TYPES, help="generated TypeScript declaration") + parser.add_argument("--example", type=pathlib.Path, default=DEFAULT_EXAMPLE, help="optional frontend example fixture") + parser.add_argument("--output", type=pathlib.Path, default=DEFAULT_OUTPUT, help="zip bundle to write") + parser.add_argument("--skip-example", action="store_true", help="omit the optional example fixture") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + config_path = args.config.resolve() + output_path = args.output.resolve() + + if config_path.name == "juno-v1-testnet.template.json": + fail("refusing to package the placeholder deployment template as a frontend release") + + config = load_config(config_path) + assert_not_template(config) + + run([sys.executable, str(CHECK_TEMPLATE), str(config_path)]) + run([sys.executable, str(CHECK_FRONTEND), str(config_path)]) + run([sys.executable, str(GENERATE_TYPES), "--check"]) + if not args.skip_example: + run([sys.executable, str(CHECK_EXAMPLE)]) + run([sys.executable, str(CHECK_SYNC)]) + + entries = { + BUNDLE_CONFIG_NAME: json.dumps(config, indent=2, sort_keys=True).encode("utf-8") + b"\n", + BUNDLE_TYPES_NAME: read_bytes(args.types.resolve()), + } + if not args.skip_example: + entries[BUNDLE_EXAMPLE_NAME] = read_bytes(args.example.resolve()) + assert_bundle_scope(entries) + + manifest = build_manifest(entries, config) + manifest_bytes = json.dumps(manifest, indent=2, sort_keys=True).encode("utf-8") + b"\n" + + output_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for name, data in entries.items(): + archive.writestr(name, data) + archive.writestr(BUNDLE_MANIFEST_NAME, manifest_bytes) + + print(f"OK: wrote Astroport-Juno v1 frontend release bundle to {output_path}") + print( + f"bundle_files={len(entries)} manifest={BUNDLE_MANIFEST_NAME} " + f"required_addresses={len(manifest['required_frontend_addresses'])} optional_addresses={len(manifest['optional_frontend_addresses'])}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_artifacts.py b/scripts/check_juno_v1_artifacts.py new file mode 100755 index 000000000..dab7083e0 --- /dev/null +++ b/scripts/check_juno_v1_artifacts.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Check optimized wasm artifacts against the Astroport-Juno v1 contract set. + +This guard is intentionally dependency-free so CI can run it immediately after +rust-optimizer. It catches the release-risk class where the workspace or build +container emits a deferred contract wasm even though docs/schemas describe the +smaller Juno v1 surface. +""" +from __future__ import annotations + +import argparse +import pathlib +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +EXPECTED_ARTIFACTS = { + "astroport_factory.wasm", + "astroport_incentives.wasm", + "astroport_native_coin_registry.wasm", + "astroport_oracle.wasm", + "astroport_pair.wasm", + "astroport_router.wasm", + "astroport_tokenfactory_tracker.wasm", + "astroport_whitelist.wasm", +} + +FORBIDDEN_ARTIFACT_FRAGMENTS = ( + "converter", + "maker", + "pair_concentrated", + "pair_stable", + "pair_xastro", + "sale_tax", + "staking", + "vesting", + "xastro_token", +) + + +def fail(msg: str) -> None: + print(f"FAIL: {msg}") + sys.exit(1) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "artifacts_dir", + nargs="?", + default=str(ROOT / "artifacts"), + help="Directory containing optimized .wasm artifacts (default: ./artifacts)", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + artifacts_dir = pathlib.Path(args.artifacts_dir) + if not artifacts_dir.exists(): + fail(f"artifacts directory is missing: {artifacts_dir}") + if not artifacts_dir.is_dir(): + fail(f"artifacts path is not a directory: {artifacts_dir}") + + actual = {p.name for p in artifacts_dir.glob("*.wasm") if p.is_file()} + missing = EXPECTED_ARTIFACTS - actual + extra = actual - EXPECTED_ARTIFACTS + forbidden = sorted( + name + for name in actual + if any(fragment in name for fragment in FORBIDDEN_ARTIFACT_FRAGMENTS) + ) + + if missing: + fail(f"missing v1 artifact(s): {sorted(missing)}") + if forbidden: + fail(f"forbidden deferred artifact(s): {forbidden}") + if extra: + fail(f"unexpected artifact(s): {sorted(extra)}") + + print("OK: optimized artifacts match Astroport-Juno v1 contract set") + print(f"artifact_count={len(actual)} expected={len(EXPECTED_ARTIFACTS)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_ci_wiring.py b/scripts/check_juno_v1_ci_wiring.py new file mode 100644 index 000000000..bb58145c8 --- /dev/null +++ b/scripts/check_juno_v1_ci_wiring.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Validate CI wiring for Astroport-Juno v1 launch guards. + +This is intentionally dependency-free: it scans the GitHub Actions workflow text +for the no-network guard commands and their ordering relative to expensive Rust +/ optimizer work. It catches accidental workflow edits that would leave the Juno +v1 scope/template/artifact guards documented but not actually enforced in CI. +""" +from __future__ import annotations + +import pathlib +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +WORKFLOWS = ROOT / ".github" / "workflows" +TESTS = WORKFLOWS / "tests_and_checks.yml" +ARTIFACTS = WORKFLOWS / "check_artifacts.yml" +RELEASE_ARTIFACTS = WORKFLOWS / "release_artifacts.yml" + + +class WorkflowText: + def __init__(self, path: pathlib.Path) -> None: + self.path = path + try: + self.lines = path.read_text().splitlines() + except FileNotFoundError: + fail(f"missing workflow: {path.relative_to(ROOT)}") + + def first(self, needle: str) -> int: + for idx, line in enumerate(self.lines, start=1): + if needle in line: + return idx + fail(f"{self.path.relative_to(ROOT)} missing: {needle}") + + def all(self, needle: str) -> list[int]: + hits = [idx for idx, line in enumerate(self.lines, start=1) if needle in line] + if not hits: + fail(f"{self.path.relative_to(ROOT)} missing: {needle}") + return hits + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}") + sys.exit(1) + + +def assert_before(name: str, left_line: int, right_line: int) -> None: + if left_line >= right_line: + fail(f"expected {name} before line {right_line}, got line {left_line}") + + +def main() -> None: + tests = WorkflowText(TESTS) + artifacts = WorkflowText(ARTIFACTS) + release_artifacts = WorkflowText(RELEASE_ARTIFACTS) + + scope_line = tests.first("scripts/check_juno_v1_scope.py") + schema_lines = tests.all("scripts/check_juno_v1_schemas.py") + template_line = tests.first("scripts/check_juno_v1_deployment_template.py") + tx_extractor_line = tests.first("scripts/check_juno_v1_tx_extractor.py") + deployment_command_line = tests.first("scripts/check_juno_v1_deployment_command.py") + secret_scan_line = tests.first("scripts/check_juno_v1_secret_scan.py") + operator_checklist_line = tests.first("scripts/check_juno_v1_operator_checklist.py") + dry_run_txs_line = tests.first("scripts/check_juno_v1_dry_run_txs.py") + deployment_gitignore_line = tests.first("scripts/check_juno_v1_deployment_gitignore.py") + deployment_readme_line = tests.first("scripts/check_juno_v1_deployment_readme.py") + frontend_config_line = tests.first("scripts/check_juno_v1_frontend_config.py") + frontend_types_line = tests.first("scripts/generate_juno_v1_frontend_types.py --check") + frontend_example_line = tests.first("scripts/check_juno_v1_frontend_example.py") + frontend_handoff_sync_line = tests.first("scripts/check_juno_v1_frontend_handoff_sync.py") + frontend_release_checklist_line = tests.first("scripts/check_juno_v1_frontend_release_checklist.py") + ci_wiring_line = tests.first("scripts/check_juno_v1_ci_wiring.py") + rust_line = tests.first("dtolnay/rust-toolchain@stable") + build_schemas_line = tests.first("scripts/build_schemas.sh") + diff_index_line = tests.first("git diff-index --cached HEAD --exit-code") + + for label, line in ( + ("scope guard", scope_line), + ("schema guard", schema_lines[0]), + ("deployment template guard", template_line), + ("tx extractor fixture guard", tx_extractor_line), + ("deployment command guard", deployment_command_line), + ("secret scan guard", secret_scan_line), + ("operator checklist guard", operator_checklist_line), + ("dry-run tx rehearsal guard", dry_run_txs_line), + ("deployment gitignore guard", deployment_gitignore_line), + ("deployment README guard", deployment_readme_line), + ("frontend config guard", frontend_config_line), + ("frontend TypeScript handoff guard", frontend_types_line), + ("frontend TypeScript example guard", frontend_example_line), + ("frontend handoff sync guard", frontend_handoff_sync_line), + ("frontend release checklist guard", frontend_release_checklist_line), + ("CI wiring guard", ci_wiring_line), + ): + assert_before(label, line, rust_line) + + if not ( + scope_line + < schema_lines[0] + < template_line + < tx_extractor_line + < deployment_command_line + < secret_scan_line + < operator_checklist_line + < dry_run_txs_line + < deployment_gitignore_line + < deployment_readme_line + < frontend_config_line + < frontend_types_line + < frontend_example_line + < frontend_handoff_sync_line + < frontend_release_checklist_line + < ci_wiring_line + ): + fail("tests workflow must run launch guards in scope/schema/template/tx-extractor/deployment-command/secret-scan/operator-checklist/dry-run-txs/deployment-gitignore/deployment-readme/frontend-config/frontend-types/frontend-example/frontend-handoff-sync/frontend-release-checklist/ci-wiring order") + + if len(schema_lines) < 2: + fail("tests workflow must run schema guard both before Rust work and after schema generation") + if not (build_schemas_line < schema_lines[-1] < diff_index_line): + fail( + "post-generation schema guard must run after build_schemas.sh and before git diff-index" + ) + + build_artifacts_line = artifacts.first("name: Build Artifacts") + size_line = artifacts.first("scripts/check_artifacts_size.sh") + artifact_guard_line = artifacts.first("scripts/check_juno_v1_artifacts.py") + upload_line = artifacts.first("actions/upload-artifact") + download_line = artifacts.first("actions/download-artifact") + cosmwasm_check_line = artifacts.all("cosmwasm-check $GITHUB_WORKSPACE/artifacts/*.wasm")[-1] + + if not (build_artifacts_line < size_line < artifact_guard_line < upload_line < download_line < cosmwasm_check_line): + fail("artifact workflow must build, size-check, v1 artifact-check, upload per-run artifacts, download them, then cosmwasm-check") + for idx, line in enumerate(artifacts.lines, start=1): + if "path: artifacts" in line: + before = "\n".join(artifacts.lines[max(0, idx - 5) : idx]) + if "actions/upload-artifact" not in before and "actions/download-artifact" not in before: + fail("artifact workflow must only pass artifacts via upload/download-artifact") + for idx, line in enumerate(release_artifacts.lines, start=1): + if "path: artifacts" in line: + before = "\n".join(release_artifacts.lines[max(0, idx - 5) : idx]) + if "actions/cache" in before: + fail("release_artifacts.yml must not restore cached artifacts before packaging a release") + + print("OK: GitHub Actions wiring enforces Astroport-Juno v1 guards") + print( + "tests_guards=scope/schema/template/tx-extractor/deployment-command/secret-scan/operator-checklist pre_rust=true " + "dry_run_txs=true deployment_gitignore=true deployment_readme=true frontend_config=true frontend_types=true frontend_example=true frontend_handoff_sync=true frontend_release_checklist=true schema_post_generation=true artifact_guard_after_size=true artifact_handoff=upload-download release_artifacts_no_cache=true" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_deployment_command.py b/scripts/check_juno_v1_deployment_command.py new file mode 100644 index 000000000..6932fe313 --- /dev/null +++ b/scripts/check_juno_v1_deployment_command.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Self-test final Astroport-Juno v1 deployment command bundling.""" +from __future__ import annotations + +import pathlib +import json +import subprocess +import sys +import tempfile +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BUILDER = ROOT / "scripts" / "build_juno_v1_deployment_command.py" +MAINNET_GUIDE = ROOT / "deployment" / "MAINNET_DEPLOYMENT.md" +READINESS_PLAN = ROOT / "deployment" / "juno-v1-readiness-plan.md" + +TX_SET_LINES = [ + "--set code_ids.astroport-factory='101'", + "--set code_ids.astroport-incentives='102'", + "--set code_ids.astroport-native-coin-registry='103'", + "--set code_ids.astroport-oracle='104'", + "--set code_ids.astroport-pair='105'", + "--set code_ids.astroport-router='106'", + "--set code_ids.astroport-tokenfactory-tracker='107'", + "--set code_ids.astroport-whitelist='108'", + "--set code_ids.cw20-base='109'", + "--set addresses.astroport-factory='juno1factory000000000000000000000000000000000'", + "--set addresses.astroport-incentives='juno1incentives00000000000000000000000000000'", + "--set addresses.astroport-native-coin-registry='juno1registry0000000000000000000000000000000'", + "--set addresses.astroport-oracle='juno1oracle0000000000000000000000000000000000'", + "--set addresses.astroport-router='juno1router0000000000000000000000000000000000'", + "--set addresses.astroport-tokenfactory-tracker='juno1tracker00000000000000000000000000000000'", + "--set addresses.astroport-whitelist='juno1whitelist000000000000000000000000000000'", +] + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def run(args: list[str], *, expect_ok: bool = True) -> subprocess.CompletedProcess[str]: + proc = subprocess.run( + [sys.executable, str(BUILDER), *args], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if expect_ok and proc.returncode != 0: + fail(f"builder failed for {args!r}: stdout={proc.stdout!r} stderr={proc.stderr!r}") + if not expect_ok and proc.returncode == 0: + fail(f"builder unexpectedly succeeded for {args!r}: stdout={proc.stdout!r}") + return proc + + +def main() -> None: + with tempfile.TemporaryDirectory(prefix="juno-v1-deployment-command-") as raw_tmp: + tmp = pathlib.Path(raw_tmp) + tx_sets = tmp / "tx-sets.txt" + rendered = tmp / "juno-v1-filled.json" + tx_sets.write_text("\n".join(TX_SET_LINES) + "\n") + + common_args = [ + "--tx-sets", + str(tx_sets), + "--owner", + "juno1owner0000000000000000000000000000000000", + "--guardian", + "juno1guardian00000000000000000000000000000000", + "--treasury", + "juno1treasury00000000000000000000000000000000", + "--tokenfactory-module", + "juno1factorymodule0000000000000000000000000000", + "--counterparty-denom", + "ibc/0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", + "--output", + str(rendered), + ] + + dry = run(common_args).stdout + for needle in ( + "scripts/fill_juno_v1_deployment_config.py", + "--require-complete", + "--set", + "sets=24 tx_sets=16 manual_sets=5 network=uni-7 render=False", + ): + if needle not in dry: + fail(f"dry command output missing {needle!r}: {dry!r}") + + rendered_proc = run([*common_args, "--render"]).stdout + for needle in ( + "sets=24 tx_sets=16 manual_sets=5 network=uni-7 render=True", + "OK: wrote rendered Juno v1 deployment config", + "OK: Juno v1 deployment template matches instantiate schema requirements", + "first_pool_gate=permissioned", + ): + if needle not in rendered_proc: + fail(f"render output missing {needle!r}: {rendered_proc!r}") + if not rendered.exists(): + fail("render mode did not create output config") + + mainnet_rendered = tmp / "juno-v1-mainnet.json" + mainnet_proc = run([*common_args[:-2], "--output", str(mainnet_rendered), "--network", "juno-1", "--render"]).stdout + if "network=juno-1" not in mainnet_proc: + fail(f"mainnet render did not report juno-1 network: {mainnet_proc!r}") + mainnet = json.loads(mainnet_rendered.read_text()) + expected_mainnet_network = { + "chain_id": "juno-1", + "bech32_prefix": "juno", + "fee_denom": "ujuno", + "native_asset_denom": "ujuno", + } + if mainnet.get("network") != expected_mainnet_network: + fail(f"mainnet render inherited wrong network: {mainnet.get('network')!r}") + factory_pair_config = mainnet["instantiate_msgs"]["astroport-factory"]["pair_configs"][0] + if factory_pair_config.get("permissioned") is not True: + fail("mainnet factory instantiate must keep XYK permissioned before the first-pool gate") + final_pair_config = mainnet["post_update_state"]["astroport-factory"]["pair_configs"][0] + if final_pair_config.get("permissioned") is not False: + fail("mainnet post-update state must document permissionless opening after the first-pool gate") + + unsafe_mainnet = run([*common_args[:-2], "--output", str(mainnet_rendered), "--render"], expect_ok=False) + if "--network juno-1" not in unsafe_mainnet.stderr: + fail(f"mainnet output without network override was not rejected: {unsafe_mainnet.stderr!r}") + + incomplete = tmp / "incomplete-tx-sets.txt" + incomplete.write_text("\n".join(TX_SET_LINES[:-1]) + "\n") + bad = run([*common_args[:1], str(incomplete), *common_args[2:]], expect_ok=False) + if "tx sets missing required deployment values" not in bad.stderr: + fail(f"incomplete tx-set failure was not explicit: {bad.stderr!r}") + + mainnet_guide = MAINNET_GUIDE.read_text() + for needle in ( + "--network juno-1", + "deployment/juno-v1-mainnet.json", + "junod query tx", + "save the included tx response", + "Keep XYK pair creation permissioned", + "Query factory pair registry and pool balances", + ): + if needle not in mainnet_guide: + fail(f"mainnet deployment guide missing required text: {needle}") + + readiness_plan = READINESS_PLAN.read_text() + for needle in ( + "`permissioned=true` during the first-pool gate", + "official first pair is registered, seeded, and smoke-checked", + "Open public pair creation only after the first-pool gate passes", + ): + if needle not in readiness_plan: + fail(f"readiness plan missing first-pool gate text: {needle}") + + print("OK: Juno v1 deployment command builder combines tx sets and manual values") + print("sets=24 tx_sets=16 manual_sets=5 network_sets=3 render_guard=true failure_cases=2 mainnet_network=juno-1") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_deployment_gitignore.py b/scripts/check_juno_v1_deployment_gitignore.py new file mode 100644 index 000000000..927c80b21 --- /dev/null +++ b/scripts/check_juno_v1_deployment_gitignore.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Validate gitignore safety rails for Astroport-Juno v1 deployment output. + +Real uni-7 tx JSON and rendered deployment configs are operator/local artifacts. +This guard keeps those paths out of git so rehearsals and real deployment output +cannot be accidentally committed as source-of-truth contract changes. +""" +from __future__ import annotations + +import pathlib +import subprocess +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +GITIGNORE = ROOT / ".gitignore" +GENERATOR = ROOT / "scripts" / "generate_juno_v1_dry_run_txs.py" +README = ROOT / "deployment" / "README.md" + +REQUIRED_GITIGNORE_LINES = ( + "/deployment/tx/", + "/deployment/juno-v1-testnet.json", + "/deployment/juno-v1-mainnet.json", +) + +SHOULD_BE_IGNORED = ( + "deployment/tx/uni-7/store-astroport-factory.json", + "deployment/tx/uni-7/instantiate-astroport-factory.json", + "deployment/tx/uni-7/tx-sets.txt", + "deployment/tx/uni-7-dry-run/store-astroport-factory.json", + "deployment/juno-v1-testnet.json", + "deployment/juno-v1-mainnet.json", +) + +FORBIDDEN_TRACKED_PREFIXES = ( + "deployment/tx/", +) + +FORBIDDEN_TRACKED_FILES = ( + "deployment/juno-v1-testnet.json", + "deployment/juno-v1-mainnet.json", +) + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def run_git(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", *args], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +def require_ignored(path: str) -> None: + proc = run_git(["check-ignore", "--no-index", path]) + if proc.returncode != 0: + fail(f"expected git to ignore {path}; stderr={proc.stderr.strip()!r}") + + +def main() -> None: + gitignore = GITIGNORE.read_text() + for line in REQUIRED_GITIGNORE_LINES: + if line not in gitignore.splitlines(): + fail(f".gitignore missing required deployment artifact ignore: {line}") + + for path in SHOULD_BE_IGNORED: + require_ignored(path) + + tracked = run_git(["ls-files", "--", "deployment/tx", "deployment/juno-v1-testnet.json", "deployment/juno-v1-mainnet.json"]) + if tracked.returncode != 0: + fail(f"git ls-files failed: {tracked.stderr.strip()}") + tracked_paths = [line.strip() for line in tracked.stdout.splitlines() if line.strip()] + forbidden = [ + path + for path in tracked_paths + if path in FORBIDDEN_TRACKED_FILES or any(path.startswith(prefix) for prefix in FORBIDDEN_TRACKED_PREFIXES) + ] + if forbidden: + fail("deployment local artifacts are tracked: " + ", ".join(forbidden)) + + generator = GENERATOR.read_text() + if 'default=pathlib.Path("deployment/tx/uni-7-dry-run")' not in generator: + fail("dry-run tx generator default output must stay under ignored deployment/tx/") + + readme = README.read_text() + for needle in ( + "ignored directory", + "deployment/tx/uni-7-dry-run", + "juno-v1-testnet.json` — suggested rendered output path; do not commit real values", + "juno-v1-mainnet.json` — rendered mainnet output path; do not commit real values", + ): + if needle not in readme: + fail(f"deployment README missing gitignore safety text: {needle}") + + print("OK: Juno v1 deployment tx/output paths stay gitignored") + print(f"ignored_paths={len(SHOULD_BE_IGNORED)} tracked_artifacts=0 generator_default=deployment/tx/uni-7-dry-run") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_deployment_readme.py b/scripts/check_juno_v1_deployment_readme.py new file mode 100644 index 000000000..a54d6940c --- /dev/null +++ b/scripts/check_juno_v1_deployment_readme.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Validate the Astroport-Juno v1 deployment README handoff. + +The README is the operator/frontend bridge for uni-7. This guard keeps its +manual value checklist, render command, dry-run rehearsal, and frontend +consumption snippet aligned with the dependency-free deployment helpers. +""" +from __future__ import annotations + +import pathlib +import re +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +README = ROOT / "deployment" / "README.md" +TYPES = ROOT / "deployment" / "juno-v1-frontend-config.d.ts" +EXAMPLE = ROOT / "deployment" / "juno-v1-frontend-config.example.ts" +TEMPLATE = ROOT / "deployment" / "juno-v1-testnet.template.json" + +ACCOUNT_SETS = ( + "accounts.owner", + "accounts.guardian", + "accounts.treasury", + "accounts.tokenfactory_module", +) +CODE_ID_SETS = ( + "code_ids.astroport-factory", + "code_ids.astroport-incentives", + "code_ids.astroport-native-coin-registry", + "code_ids.astroport-oracle", + "code_ids.astroport-pair", + "code_ids.astroport-router", + "code_ids.astroport-tokenfactory-tracker", + "code_ids.astroport-whitelist", + "code_ids.cw20-base", +) +ADDRESS_SETS = ( + "addresses.astroport-factory", + "addresses.astroport-incentives", + "addresses.astroport-native-coin-registry", + "addresses.astroport-oracle", + "addresses.astroport-router", + "addresses.astroport-tokenfactory-tracker", + "addresses.astroport-whitelist", +) +PAIR_SET = "pair_create_msg_template.asset_infos.1.native_token.denom" +REQUIRED_SECTIONS = ( + "## Required values after upload / instantiate", + "## Extract values from tx JSON", + "## Render command shape", + "## Frontend consumption", + "## Scope guardrails", +) +REQUIRED_COMMANDS = ( + "python3 scripts/generate_juno_v1_dry_run_txs.py --output-dir deployment/tx/uni-7-dry-run", + "python3 scripts/check_juno_v1_dry_run_txs.py", + "python3 scripts/extract_juno_v1_tx_sets.py", + "python3 scripts/fill_juno_v1_deployment_config.py", + "python3 scripts/check_juno_v1_deployment_template.py deployment/juno-v1-testnet.json", +) +FRONTEND_SNIPPET = ( + 'import deployment from "./juno-v1-testnet.json";', + 'import type { JunoV1FrontendDeploymentConfig } from "./juno-v1-frontend-config";', + "const config = deployment satisfies JunoV1FrontendDeploymentConfig;", +) +SCOPE_GUARDRAILS = ( + "v1 is XYK-only and permissionless.", + "No new DEX token is introduced; incentives use the configured native denom.", + "Do not add stable pairs, LSTs, perps, or yield surfaces to this config.", + "discover pools through factory queries", +) + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def require_once(text: str, needle: str, label: str | None = None) -> None: + count = text.count(needle) + if count != 1: + fail(f"expected exactly one {label or needle!r}, found {count}") + + +def main() -> None: + try: + text = README.read_text() + except FileNotFoundError: + fail("missing deployment/README.md") + + for path in (TYPES, EXAMPLE, TEMPLATE): + if not path.exists(): + fail(f"README references handoff file that is missing: {path.relative_to(ROOT)}") + + for section in REQUIRED_SECTIONS: + require_once(text, section, f"README section {section}") + + for needle in REQUIRED_COMMANDS + FRONTEND_SNIPPET + SCOPE_GUARDRAILS: + if needle not in text: + fail(f"README missing required handoff text: {needle}") + + for key in ACCOUNT_SETS + CODE_ID_SETS + ADDRESS_SETS + (PAIR_SET,): + # Each required value should appear once in the checklist and once in the + # render command, preventing one side of the handoff from drifting. + if text.count(key) < 2: + fail(f"README must mention {key} in both checklist and render command") + require_once(text, f"--set {key}=", f"render --set for {key}") + + if "operator-tx-checklist.md" not in text: + fail("README must link the operator tx checklist") + if "juno-v1-frontend-config.example.ts" not in text: + fail("README must link the frontend TypeScript example") + if not re.search(r"first pool form can start from `config\.pair_create_msg_template`", text): + fail("README must keep first-pool template as a form seed, not a hardcoded pool") + if re.search(r"stable|PCL|LST|perps|yield", text, flags=re.IGNORECASE) and "Do not add stable pairs, LSTs, perps, or yield surfaces" not in text: + fail("README has deferred-scope words without the explicit v1 guardrail") + + print("OK: Juno v1 deployment README matches operator/frontend handoff helpers") + print( + f"account_sets={len(ACCOUNT_SETS)} code_id_sets={len(CODE_ID_SETS)} " + f"address_sets={len(ADDRESS_SETS)} frontend_snippet=true scope_guardrails=true" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_deployment_template.py b/scripts/check_juno_v1_deployment_template.py new file mode 100644 index 000000000..7cc83e2df --- /dev/null +++ b/scripts/check_juno_v1_deployment_template.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Validate the Astroport-Juno v1 deployment config template. + +This deliberately avoids jsonschema dependencies. It checks the parts that reduce +launch risk here: every v1 instantiate message exists, all schema-required fields +are present, code IDs/addresses are wired for the exact v1 contract set, and the +factory/pair templates stay XYK-only with a permissioned first-pool launch gate. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "deployment" / "juno-v1-testnet.template.json" +SCHEMAS = ROOT / "schemas" + +EXPECTED_CONTRACTS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +) +EXPECTED_CODE_IDS = EXPECTED_CONTRACTS + ("astroport-pair", "cw20-base") +EXPECTED_ADDRESSES = EXPECTED_CONTRACTS +FORBIDDEN_PAIR_TYPES = {"stable", "custom", "concentrated"} +LEGACY_INCENTIVES_KEYS = {"astro_token", "vesting_contract"} + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}") + sys.exit(1) + + +def load_json(path: pathlib.Path) -> dict: + try: + return json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing {path.relative_to(ROOT)}") + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") + + +def required_fields(contract: str) -> set[str]: + schema = load_json(SCHEMAS / contract / "raw" / "instantiate.json") + return set(schema.get("required", [])) + + +def assert_exact_keys(name: str, actual: dict, expected: tuple[str, ...]) -> None: + actual_keys = set(actual) + expected_keys = set(expected) + missing = sorted(expected_keys - actual_keys) + extra = sorted(actual_keys - expected_keys) + if missing or extra: + fail(f"{name} key mismatch: missing={missing} extra={extra}") + + +def pair_type_keys(pair_type: object) -> set[str]: + if not isinstance(pair_type, dict): + fail(f"pair_type must be an object, got {type(pair_type).__name__}") + return set(pair_type) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "config", + nargs="?", + type=pathlib.Path, + default=TEMPLATE, + help="deployment config JSON to validate (default: deployment/juno-v1-testnet.template.json)", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + cfg = load_json(args.config) + + for top in ("network", "accounts", "code_ids", "addresses", "instantiate_msgs", "pair_create_msg_template", "frontend"): + if top not in cfg: + fail(f"missing top-level section: {top}") + + assert_exact_keys("code_ids", cfg["code_ids"], EXPECTED_CODE_IDS) + assert_exact_keys("addresses", cfg["addresses"], EXPECTED_ADDRESSES) + assert_exact_keys("instantiate_msgs", cfg["instantiate_msgs"], EXPECTED_CONTRACTS) + + for contract in EXPECTED_CONTRACTS: + msg = cfg["instantiate_msgs"].get(contract) + if not isinstance(msg, dict): + fail(f"instantiate_msgs.{contract} must be an object") + if contract == "astroport-incentives": + legacy = sorted(LEGACY_INCENTIVES_KEYS & set(msg)) + if legacy: + fail("instantiate_msgs.astroport-incentives uses legacy key(s): " + ", ".join(legacy)) + missing = sorted(required_fields(contract) - set(msg)) + if missing: + fail(f"instantiate_msgs.{contract} missing required field(s): {missing}") + + incentives_msg = cfg["instantiate_msgs"]["astroport-incentives"] + incentives_required = required_fields("astroport-incentives") + if "reward_token" not in incentives_required or incentives_required & LEGACY_INCENTIVES_KEYS: + fail( + "schemas/astroport-incentives/raw/instantiate.json is stale; " + "expected reward_token and no astro_token/vesting_contract" + ) + + factory_msg = cfg["instantiate_msgs"]["astroport-factory"] + if factory_msg.get("generator_address") is not None: + fail("factory instantiate generator_address must be null; set it via post-update update_config") + post_update_state = cfg.get("post_update_state") + if not isinstance(post_update_state, dict): + fail("missing post_update_state section") + factory_final = post_update_state.get("astroport-factory") + if not isinstance(factory_final, dict): + fail("post_update_state.astroport-factory must be an object") + expected_generator = cfg["addresses"]["astroport-incentives"] + if factory_final.get("generator_address") != expected_generator: + fail("post_update_state.astroport-factory.generator_address must equal addresses.astroport-incentives") + pair_configs = factory_msg.get("pair_configs") + if not isinstance(pair_configs, list) or len(pair_configs) != 1: + fail("factory pair_configs must contain exactly one v1 XYK config") + pair_config = pair_configs[0] + keys = pair_type_keys(pair_config.get("pair_type")) + if keys != {"xyk"}: + fail(f"factory pair_type must be XYK-only, got {sorted(keys)}") + if pair_config.get("permissioned") is not True: + fail("factory instantiate XYK pair config must stay permissioned until the official first pool is seeded") + if pair_config.get("is_disabled") is True: + fail("factory XYK pair config must not be disabled") + if keys & FORBIDDEN_PAIR_TYPES: + fail(f"factory contains forbidden pair type(s): {sorted(keys & FORBIDDEN_PAIR_TYPES)}") + + final_pair_configs = factory_final.get("pair_configs") + if not isinstance(final_pair_configs, list) or len(final_pair_configs) != 1: + fail("post_update_state.astroport-factory.pair_configs must contain exactly one v1 XYK config") + final_pair_config = final_pair_configs[0] + final_keys = pair_type_keys(final_pair_config.get("pair_type")) + if final_keys != {"xyk"}: + fail(f"post-update factory pair_type must be XYK-only, got {sorted(final_keys)}") + if final_pair_config.get("permissioned") is not False: + fail("post-update factory XYK pair config must open permissionless creation only after the first-pool gate") + if final_pair_config.get("code_id") != pair_config.get("code_id"): + fail("post-update factory XYK pair code_id must match instantiate pair config") + launch_gate = factory_final.get("first_pool_launch_gate") + if not isinstance(launch_gate, str) or "seed" not in launch_gate or "permissioned=false" not in launch_gate: + fail("post_update_state.astroport-factory.first_pool_launch_gate must document the seed-liquidity gate before permissioned=false") + + create_template = cfg["pair_create_msg_template"] + create_keys = pair_type_keys(create_template.get("pair_type")) + if create_keys != {"xyk"}: + fail(f"pair_create_msg_template pair_type must be XYK-only, got {sorted(create_keys)}") + if "asset_infos" not in create_template: + fail("pair_create_msg_template missing asset_infos") + if "init_params" not in create_template: + fail("pair_create_msg_template missing init_params") + + required_frontend = set(cfg["frontend"].get("required_addresses", [])) + missing_frontend = required_frontend - set(cfg["addresses"]) + if missing_frontend: + fail(f"frontend.required_addresses missing from addresses: {sorted(missing_frontend)}") + + print("OK: Juno v1 deployment template matches instantiate schema requirements") + print( + f"instantiate_msgs={len(EXPECTED_CONTRACTS)} code_ids={len(EXPECTED_CODE_IDS)} " + f"addresses={len(EXPECTED_ADDRESSES)} pair_type=xyk first_pool_gate=permissioned" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_dry_run_txs.py b/scripts/check_juno_v1_dry_run_txs.py new file mode 100755 index 000000000..881f36efc --- /dev/null +++ b/scripts/check_juno_v1_dry_run_txs.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Smoke-test the Astroport-Juno v1 dry-run tx fixture generator.""" +from __future__ import annotations + +import pathlib +import subprocess +import sys +import tempfile + +ROOT = pathlib.Path(__file__).resolve().parents[1] +GENERATOR = ROOT / "scripts" / "generate_juno_v1_dry_run_txs.py" +EXTRACTOR = ROOT / "scripts" / "extract_juno_v1_tx_sets.py" +BUILDER = ROOT / "scripts" / "build_juno_v1_deployment_command.py" +CHECK_FRONTEND = ROOT / "scripts" / "check_juno_v1_frontend_config.py" + +STORE_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-pair", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", + "cw20-base", +) + +ADDRESS_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +) + + +def run(args: list[str], cwd: pathlib.Path = ROOT) -> subprocess.CompletedProcess[str]: + proc = subprocess.run(args, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if proc.returncode != 0: + raise SystemExit( + f"FAIL: command failed: {' '.join(args)}\nstdout={proc.stdout}\nstderr={proc.stderr}" + ) + return proc + + +def main() -> None: + with tempfile.TemporaryDirectory(prefix="juno-v1-dry-run-") as tmp: + tx_dir = pathlib.Path(tmp) / "tx" + out_cfg = pathlib.Path(tmp) / "juno-v1-testnet.json" + tx_sets = pathlib.Path(tmp) / "tx-sets.txt" + + gen = run([sys.executable, str(GENERATOR), "--output-dir", str(tx_dir)]) + expected_files = [tx_dir / f"store-{key}.json" for key in STORE_KEYS] + [ + tx_dir / f"instantiate-{key}.json" for key in ADDRESS_KEYS + ] + missing = [str(path) for path in expected_files if not path.exists()] + if missing: + raise SystemExit("FAIL: generator missed files: " + ", ".join(missing)) + + extractor_args = [sys.executable, str(EXTRACTOR)] + for key in STORE_KEYS: + extractor_args.extend(["--code-id", f"{key}={tx_dir / f'store-{key}.json'}"]) + for key in ADDRESS_KEYS: + extractor_args.extend(["--address", f"{key}={tx_dir / f'instantiate-{key}.json'}"]) + extracted = run(extractor_args) + tx_sets.write_text(extracted.stdout) + tx_set_lines = [line for line in extracted.stdout.splitlines() if line.strip()] + if len(tx_set_lines) != 16: + raise SystemExit(f"FAIL: expected 16 tx set lines, got {len(tx_set_lines)}") + + rendered = run( + [ + sys.executable, + str(BUILDER), + "--tx-sets", + str(tx_sets), + "--owner", + "juno1dryrunowner000000000000000000000000000", + "--guardian", + "juno1dryrunguardian000000000000000000000000", + "--treasury", + "juno1dryruntreasury000000000000000000000000", + "--tokenfactory-module", + "juno1dryruntokenfactory000000000000000000000", + "--counterparty-denom", + "ibc/DRYRUNCOUNTERPARTYDENOM0000000000000000000000000000000000000000000000000000", + "--output", + str(out_cfg), + "--render", + ] + ) + if "OK: Juno v1 deployment template matches instantiate schema requirements" not in rendered.stdout: + raise SystemExit("FAIL: rendered dry-run config did not pass template guard") + + frontend = run([sys.executable, str(CHECK_FRONTEND), str(out_cfg)]) + if "OK: Juno v1 frontend config handoff is internally consistent" not in frontend.stdout: + raise SystemExit("FAIL: rendered dry-run config did not pass frontend guard") + + print( + "OK: Juno v1 dry-run tx fixtures exercise " + "generator -> extractor -> builder -> template guard -> frontend guard" + ) + print("fixture_files=16 tx_sets=16 render_guard=true frontend_guard=true") + print(gen.stdout.splitlines()[-1]) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_frontend_config.py b/scripts/check_juno_v1_frontend_config.py new file mode 100644 index 000000000..e90ec39c6 --- /dev/null +++ b/scripts/check_juno_v1_frontend_config.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Validate frontend-facing Astroport-Juno v1 deployment config invariants. + +This guard is intentionally offline and dependency-free. It checks the handoff +surface a frontend consumes after a uni-7 render: canonical addresses are present, +instantiate messages are wired back to those addresses, and the first pool create +template stays a simple native JUNO XYK pair without hardcoded launch pools. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DEFAULT_CONFIG = ROOT / "deployment" / "juno-v1-testnet.template.json" + +REQUIRED_FRONTEND_ADDRESSES = { + "astroport-factory", + "astroport-router", + "astroport-native-coin-registry", + "astroport-incentives", +} +OPTIONAL_FRONTEND_ADDRESSES = {"astroport-oracle"} + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}") + sys.exit(1) + + +def load_json(path: pathlib.Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing config: {path}") + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path}: {exc}") + if not isinstance(data, dict): + fail("top-level config must be a JSON object") + return data + + +def require_dict(cfg: dict[str, Any], key: str) -> dict[str, Any]: + value = cfg.get(key) + if not isinstance(value, dict): + fail(f"{key} must be an object") + return value + + +def native_denom(asset_info: Any, path: str) -> str: + if not isinstance(asset_info, dict): + fail(f"{path} must be an object") + native = asset_info.get("native_token") + if not isinstance(native, dict) or not isinstance(native.get("denom"), str): + fail(f"{path} must be a native_token denom") + return native["denom"] + + +def assert_eq(path: str, actual: Any, expected: Any) -> None: + if actual != expected: + fail(f"{path} mismatch: expected {expected!r}, got {actual!r}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "config", + nargs="?", + type=pathlib.Path, + default=DEFAULT_CONFIG, + help="deployment config JSON to validate (default: deployment/juno-v1-testnet.template.json)", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + cfg = load_json(args.config) + + network = require_dict(cfg, "network") + addresses = require_dict(cfg, "addresses") + msgs = require_dict(cfg, "instantiate_msgs") + frontend = require_dict(cfg, "frontend") + pair_template = require_dict(cfg, "pair_create_msg_template") + + native = network.get("native_asset_denom") + if not isinstance(native, str) or not native: + fail("network.native_asset_denom must be a non-empty string") + + required = frontend.get("required_addresses") + optional = frontend.get("optional_addresses", []) + if not isinstance(required, list) or not all(isinstance(item, str) for item in required): + fail("frontend.required_addresses must be a string list") + if not isinstance(optional, list) or not all(isinstance(item, str) for item in optional): + fail("frontend.optional_addresses must be a string list") + + required_set = set(required) + optional_set = set(optional) + if required_set != REQUIRED_FRONTEND_ADDRESSES: + fail(f"frontend.required_addresses drifted: {sorted(required_set)}") + if optional_set != OPTIONAL_FRONTEND_ADDRESSES: + fail(f"frontend.optional_addresses drifted: {sorted(optional_set)}") + + missing = sorted((required_set | optional_set) - set(addresses)) + if missing: + fail(f"frontend address keys missing from addresses: {missing}") + + if "pools" in frontend or "pairs" in frontend: + fail("frontend config must not hardcode pools/pairs before launch; discover via factory") + discovery = frontend.get("pair_discovery") + if not isinstance(discovery, str) or "factory" not in discovery.lower() or "hardcode" not in discovery.lower(): + fail("frontend.pair_discovery must direct clients to factory discovery and avoid hardcoded pools") + + factory_addr = addresses["astroport-factory"] + incentives_addr = addresses["astroport-incentives"] + coin_registry_addr = addresses["astroport-native-coin-registry"] + router_addr = addresses["astroport-router"] + + factory_msg = require_dict(msgs, "astroport-factory") + router_msg = require_dict(msgs, "astroport-router") + incentives_msg = require_dict(msgs, "astroport-incentives") + oracle_msg = require_dict(msgs, "astroport-oracle") + + assert_eq("instantiate_msgs.astroport-factory.coin_registry_address", factory_msg.get("coin_registry_address"), coin_registry_addr) + assert_eq("instantiate_msgs.astroport-factory.generator_address", factory_msg.get("generator_address"), None) + assert_eq("instantiate_msgs.astroport-router.astroport_factory", router_msg.get("astroport_factory"), factory_addr) + assert_eq("instantiate_msgs.astroport-incentives.factory", incentives_msg.get("factory"), factory_addr) + if "reward_token" not in incentives_msg: + fail("instantiate_msgs.astroport-incentives missing reward_token") + legacy_incentives = sorted({"astro_token", "vesting_contract"} & set(incentives_msg)) + if legacy_incentives: + fail("instantiate_msgs.astroport-incentives uses legacy key(s): " + ", ".join(legacy_incentives)) + post_update_state = require_dict(cfg, "post_update_state") + factory_final = require_dict(post_update_state, "astroport-factory") + assert_eq("post_update_state.astroport-factory.generator_address", factory_final.get("generator_address"), incentives_addr) + assert_eq("instantiate_msgs.astroport-oracle.factory_contract", oracle_msg.get("factory_contract"), factory_addr) + + if router_addr == factory_addr: + fail("frontend router and factory addresses must be distinct keys/values") + + pair_type = pair_template.get("pair_type") + if not isinstance(pair_type, dict) or set(pair_type) != {"xyk"}: + fail("pair_create_msg_template.pair_type must stay XYK-only") + assets = pair_template.get("asset_infos") + if not isinstance(assets, list) or len(assets) != 2: + fail("pair_create_msg_template.asset_infos must contain exactly two assets") + if native_denom(assets[0], "pair_create_msg_template.asset_infos[0]") != native: + fail("first pair asset must be network.native_asset_denom") + counterparty = native_denom(assets[1], "pair_create_msg_template.asset_infos[1]") + if counterparty == native: + fail("first pool counterparty denom must differ from native_asset_denom") + if pair_template.get("init_params") is not None: + fail("XYK pair_create_msg_template.init_params must remain null") + + print("OK: Juno v1 frontend config handoff is internally consistent") + print( + f"required_addresses={len(required_set)} optional_addresses={len(optional_set)} " + f"native={native} pair_type=xyk factory_ref={factory_addr}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_frontend_example.py b/scripts/check_juno_v1_frontend_example.py new file mode 100644 index 000000000..b270832af --- /dev/null +++ b/scripts/check_juno_v1_frontend_example.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Validate the Astroport-Juno v1 frontend TypeScript consumer example. + +This is an offline, dependency-free guard. It does not try to replace a real +TypeScript compiler; it catches the launch-risk mistakes that matter for this +handoff fixture: importing the generated v1 type, using `satisfies`, keeping the +frontend address surface exact, preserving an XYK-only pair template, and not +teaching frontends to hardcode launch pools/pairs. +""" +from __future__ import annotations + +import json +import pathlib +import re +import sys +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "deployment" / "juno-v1-testnet.template.json" +TYPES = ROOT / "deployment" / "juno-v1-frontend-config.d.ts" +EXAMPLE = ROOT / "deployment" / "juno-v1-frontend-config.example.ts" + +FORBIDDEN_SCOPE = ( + "pair_stable", + "pair_concentrated", + "xastro", + "astro-token", + "maker", + "vesting", + "perp", + "lst", + "vault", +) + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}") + sys.exit(1) + + +def load_json(path: pathlib.Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing {path.relative_to(ROOT)}") + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") + if not isinstance(data, dict): + fail(f"{path.relative_to(ROOT)} must contain a JSON object") + return data + + +def read(path: pathlib.Path) -> str: + try: + return path.read_text() + except FileNotFoundError: + fail(f"missing {path.relative_to(ROOT)}") + + +def quoted_keys_from_record(text: str, record_name: str) -> set[str]: + match = re.search(rf"{record_name}:\s*{{(?P.*?)^ }},", text, re.S | re.M) + if not match: + fail(f"example missing {record_name} object") + return set(re.findall(r'"([a-z0-9-]+)"\s*:', match.group("body"))) + + +def quoted_values_from_array(text: str, name: str) -> list[str]: + match = re.search(rf"{name}:\s*\[(?P.*?)\]", text, re.S) + if not match: + fail(f"example missing {name} array") + return re.findall(r'"([a-z0-9-]+)"', match.group("body")) + + +def main() -> None: + template = load_json(TEMPLATE) + types = read(TYPES) + example = read(EXAMPLE) + + required_snippets = ( + "import type {", + "JunoV1FrontendDeploymentConfig", + "JunoV1FrontendAddressKey", + 'from "./juno-v1-frontend-config"', + "satisfies JunoV1FrontendDeploymentConfig", + "frontendAddressMap(", + "firstXykPairCreateMsg(", + "pair_type: { xyk: {} }", + "init_params: null", + ) + for snippet in required_snippets: + if snippet not in example: + fail(f"example missing required snippet: {snippet}") + + lowered = example.lower() + forbidden_hits = [term for term in FORBIDDEN_SCOPE if term in lowered] + if forbidden_hits: + fail(f"example includes deferred/non-v1 scope terms: {forbidden_hits}") + + if re.search(r"\b(pools|pairs):\s*\[", example): + fail("example must not include hardcoded pools/pairs arrays") + if "factory contract, not hardcoded" not in lowered: + fail("example must explicitly direct pair discovery through factory, not hardcoded pools") + + expected_code_ids = set(template.get("code_ids", {})) + expected_addresses = set(template.get("addresses", {})) + expected_required = list(template["frontend"]["required_addresses"]) + expected_optional = list(template["frontend"]["optional_addresses"]) + + code_keys = quoted_keys_from_record(example, "code_ids") + address_keys = quoted_keys_from_record(example, "addresses") + required = quoted_values_from_array(example, "required_addresses") + optional = quoted_values_from_array(example, "optional_addresses") + + if code_keys != expected_code_ids: + fail(f"example code_ids keys drifted: {sorted(code_keys)}") + if address_keys != expected_addresses: + fail(f"example addresses keys drifted: {sorted(address_keys)}") + if required != expected_required: + fail(f"example required_addresses drifted: {required}") + if optional != expected_optional: + fail(f"example optional_addresses drifted: {optional}") + + type_address_union = re.search(r"export type JunoV1AddressKey = (?P.*?);", types) + if not type_address_union: + fail("generated type file missing JunoV1AddressKey union") + type_address_keys = set(re.findall(r'"([a-z0-9-]+)"', type_address_union.group("body"))) + if address_keys != type_address_keys: + fail("example address keys do not match generated type union") + + print("OK: Juno v1 frontend TypeScript example consumes the generated handoff type") + print( + f"code_ids={len(code_keys)} addresses={len(address_keys)} " + f"required={len(required)} optional={len(optional)} pair_type=xyk" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_frontend_handoff_sync.py b/scripts/check_juno_v1_frontend_handoff_sync.py new file mode 100644 index 000000000..98f63df18 --- /dev/null +++ b/scripts/check_juno_v1_frontend_handoff_sync.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Keep the Juno v1 frontend handoff address surface synchronized. + +The deployment template is the source of truth for which contract addresses the +frontend must consume at launch. This guard checks that the generated TypeScript +unions, consumer example, and deployment README all present the same required +and optional frontend address keys. It is intentionally dependency-free so it can +run before Rust/toolchain setup in CI. +""" +from __future__ import annotations + +import json +import pathlib +import re +import sys +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +TEMPLATE = ROOT / "deployment" / "juno-v1-testnet.template.json" +TYPES = ROOT / "deployment" / "juno-v1-frontend-config.d.ts" +EXAMPLE = ROOT / "deployment" / "juno-v1-frontend-config.example.ts" +README = ROOT / "deployment" / "README.md" + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}") + sys.exit(1) + + +def read(path: pathlib.Path) -> str: + try: + return path.read_text() + except FileNotFoundError: + fail(f"missing {path.relative_to(ROOT)}") + + +def load_template() -> dict[str, Any]: + try: + data = json.loads(TEMPLATE.read_text()) + except FileNotFoundError: + fail("missing deployment/juno-v1-testnet.template.json") + except json.JSONDecodeError as exc: + fail(f"invalid deployment template JSON: {exc}") + if not isinstance(data, dict): + fail("deployment template must be a JSON object") + return data + + +def ts_union_values(text: str, type_name: str) -> list[str]: + match = re.search(rf"export type {re.escape(type_name)} = (?P.*?);", text, re.S) + if not match: + fail(f"generated types missing {type_name}") + return re.findall(r'"([a-z0-9-]+)"', match.group("body")) + + +def example_array_values(text: str, name: str) -> list[str]: + match = re.search(rf"{re.escape(name)}:\s*\[(?P.*?)\]", text, re.S) + if not match: + fail(f"frontend example missing {name} array") + return re.findall(r'"([a-z0-9-]+)"', match.group("body")) + + +def frontend_address_map_keys(text: str) -> list[str]: + match = re.search( + r"return\s*{(?P.*?)^\s*};\s*\n}\s*\n\s*export function firstXykPairCreateMsg", + text, + re.S | re.M, + ) + if not match: + fail("frontend example missing frontendAddressMap return object") + return re.findall(r'"([a-z0-9-]+)"\s*:', match.group("body")) + + +def backticked_list(values: list[str]) -> str: + return ", ".join(f"`{value}`" for value in values) + + +def main() -> None: + template = load_template() + types = read(TYPES) + example = read(EXAMPLE) + readme = read(README) + + frontend = template.get("frontend") + if not isinstance(frontend, dict): + fail("deployment template missing frontend object") + + expected_required = frontend.get("required_addresses") + expected_optional = frontend.get("optional_addresses") + if not isinstance(expected_required, list) or not all(isinstance(v, str) for v in expected_required): + fail("deployment template frontend.required_addresses must be a string array") + if not isinstance(expected_optional, list) or not all(isinstance(v, str) for v in expected_optional): + fail("deployment template frontend.optional_addresses must be a string array") + + expected_all = expected_required + expected_optional + if len(expected_all) != len(set(expected_all)): + fail("frontend required/optional address keys overlap") + + address_keys = set(template.get("addresses", {})) + missing = [key for key in expected_all if key not in address_keys] + if missing: + fail(f"frontend address keys are not present in deployment addresses: {missing}") + + type_required = ts_union_values(types, "JunoV1RequiredFrontendAddressKey") + type_optional = ts_union_values(types, "JunoV1OptionalFrontendAddressKey") + if type_required != expected_required: + fail(f"required frontend type union drifted: {type_required}") + if type_optional != expected_optional: + fail(f"optional frontend type union drifted: {type_optional}") + + example_required = example_array_values(example, "required_addresses") + example_optional = example_array_values(example, "optional_addresses") + example_map = frontend_address_map_keys(example) + if example_required != expected_required: + fail(f"example required_addresses drifted: {example_required}") + if example_optional != expected_optional: + fail(f"example optional_addresses drifted: {example_optional}") + if example_map != expected_all: + fail(f"frontendAddressMap keys must be required+optional order, got {example_map}") + + required_line = f"Required frontend addresses: {backticked_list(expected_required)}." + optional_line = f"Optional frontend addresses: {backticked_list(expected_optional)}." + if required_line not in readme: + fail(f"README missing synchronized required-address line: {required_line}") + if optional_line not in readme: + fail(f"README missing synchronized optional-address line: {optional_line}") + + print("OK: Juno v1 frontend handoff address keys are synchronized") + print( + f"required={len(expected_required)} optional={len(expected_optional)} " + f"map_keys={len(example_map)} source=deployment-template" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_frontend_release_bundle.py b/scripts/check_juno_v1_frontend_release_bundle.py new file mode 100644 index 000000000..fc76a5d68 --- /dev/null +++ b/scripts/check_juno_v1_frontend_release_bundle.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Smoke-test the Astroport-Juno v1 frontend release bundle helper.""" +from __future__ import annotations + +import hashlib +import json +import pathlib +import subprocess +import sys +import tempfile +import zipfile +from typing import Any + +ROOT = pathlib.Path(__file__).resolve().parents[1] +FILL = ROOT / "scripts" / "fill_juno_v1_deployment_config.py" +BUNDLE = ROOT / "scripts" / "build_juno_v1_frontend_release_bundle.py" + +SET_VALUES = { + "accounts.owner": "juno1bundleowner00000000000000000000000000", + "accounts.guardian": "juno1bundleguardian0000000000000000000000", + "accounts.treasury": "juno1bundletreasury0000000000000000000000", + "accounts.tokenfactory_module": "juno1bundletokenfactory000000000000000000", + "code_ids.astroport-factory": "101", + "code_ids.astroport-incentives": "102", + "code_ids.astroport-native-coin-registry": "103", + "code_ids.astroport-oracle": "104", + "code_ids.astroport-pair": "105", + "code_ids.astroport-router": "106", + "code_ids.astroport-tokenfactory-tracker": "107", + "code_ids.astroport-whitelist": "108", + "code_ids.cw20-base": "109", + "addresses.astroport-factory": "juno1bundlefactory0000000000000000000000", + "addresses.astroport-incentives": "juno1bundleincentives000000000000000000", + "addresses.astroport-native-coin-registry": "juno1bundleregistry00000000000000000000", + "addresses.astroport-oracle": "juno1bundleoracle00000000000000000000000", + "addresses.astroport-router": "juno1bundlerouter00000000000000000000000", + "addresses.astroport-tokenfactory-tracker": "juno1bundletracker000000000000000000000", + "addresses.astroport-whitelist": "juno1bundlewhitelist00000000000000000000", + "pair_create_msg_template.asset_infos.1.native_token.denom": "ibc/BUNDLECOUNTERPARTYDENOM0000000000000000000000000000000000000000000000000000", +} + +EXPECTED_FILES = { + "juno-v1-testnet.json", + "juno-v1-frontend-config.d.ts", + "juno-v1-frontend-config.example.ts", + "MANIFEST.json", +} +FORBIDDEN_FILES = { + "juno-v1-testnet.template.json", + "tx-sets.txt", +} + + +def run(args: list[str]) -> subprocess.CompletedProcess[str]: + proc = subprocess.run(args, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if proc.returncode != 0: + raise SystemExit( + f"FAIL: command failed: {' '.join(args)}\nstdout={proc.stdout}\nstderr={proc.stderr}" + ) + return proc + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def manifest_files(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]: + files = manifest.get("files") + if not isinstance(files, list): + raise SystemExit("FAIL: manifest.files must be a list") + mapped: dict[str, dict[str, Any]] = {} + for item in files: + if not isinstance(item, dict) or not isinstance(item.get("path"), str): + raise SystemExit("FAIL: manifest file entries must be objects with path") + mapped[item["path"]] = item + return mapped + + +def main() -> None: + with tempfile.TemporaryDirectory(prefix="juno-v1-frontend-bundle-") as tmp: + tmp_path = pathlib.Path(tmp) + config = tmp_path / "juno-v1-testnet.json" + bundle = tmp_path / "juno-v1-frontend-release.zip" + + fill_args = [sys.executable, str(FILL), "--output", str(config), "--require-complete"] + for key, value in sorted(SET_VALUES.items()): + fill_args.extend(["--set", f"{key}={value}"]) + fill = run(fill_args) + if "OK: wrote rendered Juno v1 deployment config" not in fill.stdout: + raise SystemExit("FAIL: fill helper did not render a complete config") + + built = run([sys.executable, str(BUNDLE), "--config", str(config), "--output", str(bundle)]) + if "OK: wrote Astroport-Juno v1 frontend release bundle" not in built.stdout: + raise SystemExit("FAIL: bundle helper did not report success") + if not bundle.exists(): + raise SystemExit("FAIL: bundle zip was not written") + + with zipfile.ZipFile(bundle, "r") as archive: + names = set(archive.namelist()) + if names != EXPECTED_FILES: + raise SystemExit(f"FAIL: unexpected bundle file set: {sorted(names)}") + forbidden = sorted(names & FORBIDDEN_FILES) + if forbidden: + raise SystemExit("FAIL: bundle includes forbidden files: " + ", ".join(forbidden)) + manifest = json.loads(archive.read("MANIFEST.json")) + file_meta = manifest_files(manifest) + for name in EXPECTED_FILES - {"MANIFEST.json"}: + data = archive.read(name) + meta = file_meta.get(name) + if not meta: + raise SystemExit(f"FAIL: manifest missing {name}") + if meta.get("bytes") != len(data): + raise SystemExit(f"FAIL: manifest byte count mismatch for {name}") + if meta.get("sha256") != sha256(data): + raise SystemExit(f"FAIL: manifest sha256 mismatch for {name}") + if manifest.get("scope") != "xyk-only, permissionless, no DEX token": + raise SystemExit("FAIL: manifest scope guardrail drifted") + if manifest.get("required_frontend_addresses") != [ + "astroport-factory", + "astroport-router", + "astroport-native-coin-registry", + "astroport-incentives", + ]: + raise SystemExit("FAIL: manifest required frontend addresses drifted") + + rejected = subprocess.run( + [ + sys.executable, + str(BUNDLE), + "--config", + str(ROOT / "deployment" / "juno-v1-testnet.template.json"), + "--output", + str(tmp_path / "bad.zip"), + ], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if rejected.returncode == 0 or "refusing to package the placeholder" not in rejected.stderr: + raise SystemExit("FAIL: bundle helper must reject the placeholder template") + + print("OK: Juno v1 frontend release bundle packages only verified handoff files") + print("bundle_entries=4 manifest_hashes=3 rejects_template=true") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_frontend_release_checklist.py b/scripts/check_juno_v1_frontend_release_checklist.py new file mode 100644 index 000000000..0dbc80d98 --- /dev/null +++ b/scripts/check_juno_v1_frontend_release_checklist.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Validate the Astroport-Juno v1 frontend release checklist. + +The checklist is the final bridge from a rendered uni-7 deployment config into a +UI repository. This guard keeps the copied file list, verification commands, +frontend address surface, and v1 scope limits aligned with the machine-checked +handoff artifacts. +""" +from __future__ import annotations + +import json +import pathlib +import re +import sys +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKLIST = ROOT / "deployment" / "frontend-release-checklist.md" +TEMPLATE = ROOT / "deployment" / "juno-v1-testnet.template.json" +TYPES = ROOT / "deployment" / "juno-v1-frontend-config.d.ts" +EXAMPLE = ROOT / "deployment" / "juno-v1-frontend-config.example.ts" + +RELEASE_FILES = ( + "juno-v1-testnet.json", + "juno-v1-frontend-config.d.ts", + "juno-v1-frontend-config.example.ts", +) +REQUIRED_COMMANDS = ( + "python3 scripts/check_juno_v1_deployment_template.py deployment/juno-v1-testnet.json", + "python3 scripts/check_juno_v1_frontend_config.py deployment/juno-v1-testnet.json", + "python3 scripts/generate_juno_v1_frontend_types.py --check", + "python3 scripts/check_juno_v1_frontend_example.py", + "python3 scripts/check_juno_v1_frontend_handoff_sync.py", +) +SCOPE_GUARDRAILS = ( + "v1 is XYK-only and permissionless.", + "No new DEX token is introduced; incentives use the configured native denom.", + "Do not add stable pairs, PCL, LSTs, perps, or yield surfaces to this handoff.", + "discover existing pools by querying the factory contract", + "Do not hardcode pools or pair addresses in the UI repo.", +) + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}") + sys.exit(1) + + +def read(path: pathlib.Path) -> str: + try: + return path.read_text() + except FileNotFoundError: + fail(f"missing {path.relative_to(ROOT)}") + + +def load_template() -> dict[str, Any]: + try: + data = json.loads(TEMPLATE.read_text()) + except FileNotFoundError: + fail("missing deployment/juno-v1-testnet.template.json") + except json.JSONDecodeError as exc: + fail(f"invalid deployment template JSON: {exc}") + if not isinstance(data, dict): + fail("deployment template must be a JSON object") + return data + + +def backticked_list(values: list[str]) -> str: + return ", ".join(f"`{value}`" for value in values) + + +def main() -> None: + text = read(CHECKLIST) + template = load_template() + + for path in (TYPES, EXAMPLE): + if not path.exists(): + fail(f"checklist references handoff file that is missing: {path.relative_to(ROOT)}") + + for section in ( + "## Release files to hand to the UI repo", + "## Pre-copy verification", + "## Frontend address surface", + "## Scope guardrails", + ): + if text.count(section) != 1: + fail(f"expected exactly one checklist section: {section}") + + for filename in RELEASE_FILES: + if text.count(f"`{filename}`") < 1: + fail(f"checklist missing release file `{filename}`") + if "Do not copy `juno-v1-testnet.template.json` as the live config." not in text: + fail("checklist must warn against publishing the placeholder template as live config") + + for command in REQUIRED_COMMANDS: + if text.count(command) != 1: + fail(f"checklist must contain verification command exactly once: {command}") + if "python3 scripts/check_juno_v1_dry_run_txs.py" not in text: + fail("checklist must mention the dry-run rehearsal for no-chain-output testing") + + frontend = template.get("frontend") + if not isinstance(frontend, dict): + fail("deployment template missing frontend object") + required = frontend.get("required_addresses") + optional = frontend.get("optional_addresses") + if not isinstance(required, list) or not all(isinstance(v, str) for v in required): + fail("deployment template frontend.required_addresses must be a string array") + if not isinstance(optional, list) or not all(isinstance(v, str) for v in optional): + fail("deployment template frontend.optional_addresses must be a string array") + + required_line = f"Required frontend addresses: {backticked_list(required)}." + optional_line = f"Optional frontend addresses: {backticked_list(optional)}." + if required_line not in text: + fail(f"checklist missing synchronized required-address line: {required_line}") + if optional_line not in text: + fail(f"checklist missing synchronized optional-address line: {optional_line}") + + for guardrail in SCOPE_GUARDRAILS: + if guardrail not in text: + fail(f"checklist missing v1 guardrail: {guardrail}") + if re.search(r"stable|PCL|LST|perps|yield", text, flags=re.IGNORECASE) and SCOPE_GUARDRAILS[2] not in text: + fail("checklist has deferred-scope words without the explicit v1 guardrail") + + print("OK: Juno v1 frontend release checklist matches the deployment handoff") + print( + f"release_files={len(RELEASE_FILES)} commands={len(REQUIRED_COMMANDS)} " + f"required={len(required)} optional={len(optional)} pair_discovery=factory" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_operator_checklist.py b/scripts/check_juno_v1_operator_checklist.py new file mode 100644 index 000000000..542674b89 --- /dev/null +++ b/scripts/check_juno_v1_operator_checklist.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Validate the Astroport-Juno v1 operator tx checklist. + +The checklist is the last human-facing handoff before a uni-7 deployment config +is rendered. This guard keeps its 16 expected tx filenames, config keys, and +command wiring aligned with the extractor/bundler scripts. +""" +from __future__ import annotations + +import pathlib +import re +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKLIST = ROOT / "deployment" / "operator-tx-checklist.md" +README = ROOT / "deployment" / "README.md" + +STORE_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-pair", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", + "cw20-base", +) + +ADDRESS_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +) + +REQUIRED_MANUAL_ENV = ( + "JUNO_OWNER", + "JUNO_GUARDIAN", + "JUNO_TREASURY", + "JUNO_TOKENFACTORY_MODULE", + "FIRST_COUNTERPARTY_DENOM", +) + +SCOPE_GUARDRAIL = "No stable pools, LSTs, perps, yield theater, or new token scope." + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def require_once(text: str, needle: str, label: str | None = None) -> None: + count = text.count(needle) + if count != 1: + fail(f"expected exactly one {label or needle!r}, found {count}") + + +def main() -> None: + try: + text = CHECKLIST.read_text() + except FileNotFoundError: + fail("missing deployment/operator-tx-checklist.md") + + readme = README.read_text() if README.exists() else "" + if "operator-tx-checklist.md" not in readme: + fail("deployment/README.md must link the operator tx checklist") + + for key in STORE_KEYS: + table_row = f"| `deployment/tx/uni-7/store-{key}.json` | `code_ids.{key}` |" + if table_row not in text: + fail(f"store tx table row missing for {key}") + require_once(text, f"--code-id {key}=deployment/tx/uni-7/store-{key}.json", f"extractor store arg for {key}") + + for key in ADDRESS_KEYS: + table_row = f"| `deployment/tx/uni-7/instantiate-{key}.json` | `addresses.{key}` |" + if table_row not in text: + fail(f"instantiate tx table row missing for {key}") + require_once(text, f"--address {key}=deployment/tx/uni-7/instantiate-{key}.json", f"extractor address arg for {key}") + + for env_name in REQUIRED_MANUAL_ENV: + if env_name not in text: + fail(f"manual environment variable missing from checklist: {env_name}") + + for needle in ( + "python3 scripts/extract_juno_v1_tx_sets.py", + "> deployment/tx/uni-7/tx-sets.txt", + "python3 scripts/build_juno_v1_deployment_command.py", + "--tx-sets deployment/tx/uni-7/tx-sets.txt", + "--output deployment/juno-v1-testnet.json", + "--render", + "OK: Juno v1 deployment template matches instantiate schema requirements", + "instantiate_msgs=7 code_ids=9 addresses=7 pair_type=xyk", + ): + if needle not in text: + fail(f"checklist missing required handoff text: {needle}") + + if not re.search(r"tx-sets\.txt` has 16 non-empty `--set \.\.\.` lines", text): + fail("checklist must state the expected 16 non-empty tx-set lines") + + if SCOPE_GUARDRAIL not in text: + fail("checklist missing explicit v1 scope guardrail") + + print("OK: Juno v1 operator tx checklist matches deployment helpers") + print(f"store_txs={len(STORE_KEYS)} instantiate_txs={len(ADDRESS_KEYS)} manual_values={len(REQUIRED_MANUAL_ENV)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_schemas.py b/scripts/check_juno_v1_schemas.py new file mode 100755 index 000000000..6b9a0443f --- /dev/null +++ b/scripts/check_juno_v1_schemas.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Check that committed JSON schemas match the Astroport-Juno v1 contract set. + +Stale schemas are launch risk: they make stripped contracts look shippable to +frontend/integration work. This guard intentionally checks only directory names, +so it can run without cargo, jq, or network access. +""" +from __future__ import annotations + +import pathlib +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SCHEMAS = ROOT / "schemas" + +EXPECTED_SCHEMA_DIRS = { + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-pair", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +} + +FORBIDDEN_SCHEMA_DIRS = { + "astro-token-converter", + "astroport-maker", + "astroport-pair-concentrated", + "astroport-pair-concentrated-duality", + "astroport-pair-concentrated-sale-tax", + "astroport-pair-converter", + "astroport-pair-stable", + "astroport-pair-xastro", + "astroport-pair-xyk-sale-tax", + "astroport-staking", + "astroport-vesting", + "astroport-xastro-token", +} + + +def fail(msg: str) -> None: + print(f"FAIL: {msg}") + sys.exit(1) + + +def main() -> None: + if not SCHEMAS.exists(): + fail("schemas/ directory is missing; run scripts/build_schemas.sh before release") + + actual = {p.name for p in SCHEMAS.iterdir() if p.is_dir()} + missing = EXPECTED_SCHEMA_DIRS - actual + extra = actual - EXPECTED_SCHEMA_DIRS + forbidden = actual & FORBIDDEN_SCHEMA_DIRS + + if missing: + fail(f"missing v1 schema dir(s): {sorted(missing)}") + if forbidden: + fail(f"forbidden stale schema dir(s): {sorted(forbidden)}") + if extra: + fail(f"unexpected schema dir(s): {sorted(extra)}") + + print("OK: committed schemas match Astroport-Juno v1 contract set") + print(f"schema_dirs={len(actual)} expected={len(EXPECTED_SCHEMA_DIRS)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_scope.py b/scripts/check_juno_v1_scope.py new file mode 100755 index 000000000..495277872 --- /dev/null +++ b/scripts/check_juno_v1_scope.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Check the Astroport-Juno v1 contract scope against the planning manifest. + +This is intentionally lightweight: no network, no cargo invocation, no third-party +packages. It catches the launch-risk class where docs say one contract set while +Cargo.toml builds another. +""" +from __future__ import annotations + +import pathlib +import sys +import tomllib + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +EXPECTED_WORKSPACE_MEMBERS = { + "packages/astroport", + "packages/astroport_juno_types", + "packages/astroport_test", + "packages/circular_buffer", + "contracts/factory", + "contracts/pair", + "contracts/router", + "contracts/whitelist", + "contracts/periphery/native_coin_registry", + "contracts/periphery/oracle", + "contracts/periphery/tokenfactory_tracker", + "contracts/tokenomics/incentives", + "integration-tests", +} + +EXPECTED_EXCLUDES = { + "contracts/pair_stable", + "contracts/pair_concentrated", + "packages/astroport_pcl_common", +} + +EXPECTED_WASMS = { + "astroport_factory.wasm", + "astroport_pair.wasm", + "astroport_router.wasm", + "astroport_native_coin_registry.wasm", + "astroport_oracle.wasm", + "astroport_tokenfactory_tracker.wasm", + "astroport_whitelist.wasm", + "astroport_incentives.wasm", +} + +FORBIDDEN_WORKSPACE_FRAGMENTS = ( + "pair_xastro", + "pair_astro_converter", + "pair_transmuter", + "sale_tax", + "maker", + "staking", + "vesting", + "xastro_token", +) + + +def fail(msg: str) -> None: + print(f"FAIL: {msg}") + sys.exit(1) + + +def main() -> None: + cargo_toml = ROOT / "Cargo.toml" + data = tomllib.loads(cargo_toml.read_text()) + workspace = data.get("workspace", {}) + members = set(workspace.get("members", [])) + excludes = set(workspace.get("exclude", [])) + + missing = EXPECTED_WORKSPACE_MEMBERS - members + extra = members - EXPECTED_WORKSPACE_MEMBERS + if missing or extra: + fail(f"workspace members drifted; missing={sorted(missing)} extra={sorted(extra)}") + + missing_excludes = EXPECTED_EXCLUDES - excludes + if missing_excludes: + fail(f"workspace excludes missing deferred contracts: {sorted(missing_excludes)}") + + forbidden = [m for m in members if any(fragment in m for fragment in FORBIDDEN_WORKSPACE_FRAGMENTS)] + if forbidden: + fail(f"forbidden v1 scope member(s): {forbidden}") + + strip_list = (ROOT / "planning" / "01-strip-list.md").read_text() + missing_wasm_mentions = sorted(w for w in EXPECTED_WASMS if w not in strip_list) + if missing_wasm_mentions: + fail(f"planning/01-strip-list.md missing wasm artifact(s): {missing_wasm_mentions}") + + print("OK: Astroport-Juno v1 scope matches Cargo.toml and planning/01-strip-list.md") + print(f"workspace_members={len(members)} expected_wasms={len(EXPECTED_WASMS)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_secret_scan.py b/scripts/check_juno_v1_secret_scan.py new file mode 100644 index 000000000..f54d48a0f --- /dev/null +++ b/scripts/check_juno_v1_secret_scan.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Reject tracked secret-like material in text files. + +The scan intentionally reports only path, line, and rule name. It never echoes the +matched secret-like value back to logs. +""" +from __future__ import annotations + +import pathlib +import re +import subprocess +import sys +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +RULES: tuple[tuple[str, re.Pattern[str]], ...] = ( + ("explicit mnemonic variable", re.compile(r"(?i)\b(USER_)?MNEMONIC\b\s*[:=]")), + ( + "private key variable", + re.compile( + r"(?i)(?:^|[\s{,\[])[\"']?(private[_ -]?key|secret[_ -]?key)[\"']?\s*[:=]" + ), + ), + ( + "seed phrase text", + re.compile(r"(?i)\b(seed phrase|mnemonic phrase|wallet mnemonic)\b"), + ), +) +REDACTED_SECRET_VALUE = re.compile( + r"(?i)(?:^|[\s{,\[])[\"']?(?:private[_ -]?key|secret[_ -]?key)[\"']?\s*[:=]\s*[\"']?[\"']?" +) +ALLOWLIST = { + "scripts/check_juno_v1_secret_scan.py", +} + + +SELF_TEST_CASES: tuple[tuple[str, str], ...] = ( + ("private key variable", '"private_key": "fixture-not-redacted"'), + ("private key variable", "private-key = 'fixture-not-redacted'"), + ("private key variable", "secret key: fixture-not-redacted"), +) +SELF_TEST_NEGATIVE_CASES = ( + '"public_key": ""', + '"private_key": ""', +) + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def git_ls_files() -> list[str]: + proc = subprocess.run( + ["git", "ls-files"], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if proc.returncode != 0: + fail(f"git ls-files failed: {proc.stderr.strip()}") + return [line for line in proc.stdout.splitlines() if line] + + +def read_text(path: pathlib.Path) -> str | None: + data = path.read_bytes() + if b"\0" in data: + return None + try: + return data.decode("utf-8") + except UnicodeDecodeError: + return None + + +def matching_rule(line: str) -> str | None: + if REDACTED_SECRET_VALUE.search(line): + return None + for rule_name, pattern in RULES: + if pattern.search(line): + return rule_name + return None + + +def run_self_tests() -> None: + rules_by_name = {name: pattern for name, pattern in RULES} + for rule_name, fixture_line in SELF_TEST_CASES: + pattern = rules_by_name.get(rule_name) + if pattern is None: + fail(f"missing self-test rule: {rule_name}") + if not pattern.search(fixture_line): + fail(f"secret scan self-test failed: {rule_name}") + + for fixture_line in SELF_TEST_NEGATIVE_CASES: + if matching_rule(fixture_line) is not None: + fail("secret scan self-test failed: benign key matched") + + +def main() -> None: + run_self_tests() + findings: list[str] = [] + for rel in git_ls_files(): + if rel in ALLOWLIST: + continue + text = read_text(ROOT / rel) + if text is None: + continue + for line_no, line in enumerate(text.splitlines(), start=1): + rule_name = matching_rule(line) + if rule_name is not None: + findings.append(f"{rel}:{line_no}: {rule_name}") + + if findings: + for finding in findings[:20]: + print(f"FAIL: secret-like tracked text: {finding}", file=sys.stderr) + if len(findings) > 20: + print(f"FAIL: ... {len(findings) - 20} additional finding(s) omitted", file=sys.stderr) + sys.exit(1) + + print("OK: tracked text passed Juno v1 secret scan") + print(f"files_scanned={len(git_ls_files())} rules={len(RULES)} findings=0") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_juno_v1_tx_extractor.py b/scripts/check_juno_v1_tx_extractor.py new file mode 100644 index 000000000..0f9e60d39 --- /dev/null +++ b/scripts/check_juno_v1_tx_extractor.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Self-test the Astroport-Juno tx JSON extraction helper. + +The extractor sits on the deployment critical path: operators paste its output +into fill_juno_v1_deployment_config.py after uni-7 uploads/instantiates. This +check uses fixture tx response shapes instead of live chain data so CI can catch +regressions before the bakeoff. +""" +from __future__ import annotations + +import json +import pathlib +import subprocess +import sys +import tempfile +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +EXTRACTOR = ROOT / "scripts" / "extract_juno_v1_tx_sets.py" + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def run(args: list[str], *, expect_ok: bool = True) -> subprocess.CompletedProcess[str]: + proc = subprocess.run( + [sys.executable, str(EXTRACTOR), *args], + cwd=ROOT, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if expect_ok and proc.returncode != 0: + fail(f"extractor failed for {args!r}: stdout={proc.stdout!r} stderr={proc.stderr!r}") + if not expect_ok and proc.returncode == 0: + fail(f"extractor unexpectedly succeeded for {args!r}: stdout={proc.stdout!r}") + return proc + + +def write_json(path: pathlib.Path, value: object) -> None: + path.write_text(json.dumps(value, indent=2) + "\n") + + +def main() -> None: + with tempfile.TemporaryDirectory(prefix="juno-v1-tx-extractor-") as raw_tmp: + tmp = pathlib.Path(raw_tmp) + store_tx = tmp / "store-factory.json" + instantiate_tx = tmp / "instantiate-factory.json" + raw_log_tx = tmp / "raw-log-router.json" + multi_code_tx = tmp / "multi-code.json" + + write_json( + store_tx, + { + "tx_response": { + "events": [ + { + "type": "store_code", + "attributes": [ + {"key": "code_id", "value": "77"}, + ], + } + ] + } + }, + ) + write_json( + instantiate_tx, + { + "logs": [ + { + "events": [ + { + "type": "instantiate", + "attributes": [ + { + "key": "_contract_address", + "value": "juno1factory000000000000000000000000000000000", + } + ], + } + ] + } + ] + }, + ) + write_json( + raw_log_tx, + { + "raw_log": json.dumps( + [ + { + "events": [ + { + "type": "instantiate", + "attributes": [ + { + "key": "contract_address", + "value": "juno1router0000000000000000000000000000000000", + } + ], + } + ] + } + ] + ) + }, + ) + write_json( + multi_code_tx, + { + "events": [ + {"type": "store_code", "attributes": [{"key": "code_id", "value": "77"}]}, + {"type": "store_code", "attributes": [{"key": "code_id", "value": "78"}]}, + ] + }, + ) + + mapped = run( + [ + "--code-id", + f"astroport-factory={store_tx}", + "--address", + f"astroport-factory={instantiate_tx}", + "--address", + f"astroport-router={raw_log_tx}", + ] + ).stdout.splitlines() + expected = [ + "--set code_ids.astroport-factory='77'", + "--set addresses.astroport-factory='juno1factory000000000000000000000000000000000'", + "--set addresses.astroport-router='juno1router0000000000000000000000000000000000'", + ] + if mapped != expected: + fail(f"mapped output mismatch:\nexpected={expected!r}\nactual={mapped!r}") + + scan = run(["--scan", str(store_tx), str(instantiate_tx), str(raw_log_tx)]).stdout + for needle in ( + "code_ids=77", + "addresses=juno1factory000000000000000000000000000000000", + "addresses=juno1router0000000000000000000000000000000000", + ): + if needle not in scan: + fail(f"scan output missing {needle!r}: {scan!r}") + + bad_key = run(["--code-id", f"not-a-contract={store_tx}"], expect_ok=False) + if "unknown code-id key" not in bad_key.stderr: + fail(f"bad-key failure did not explain key error: {bad_key.stderr!r}") + + multi = run(["--code-id", f"astroport-factory={multi_code_tx}"], expect_ok=False) + if "multiple code_ids" not in multi.stderr: + fail(f"multi-code failure did not explain ambiguity: {multi.stderr!r}") + + print("OK: Juno v1 tx extractor handles mapped, scan, raw_log, and failure cases") + print("fixtures=4 mapped_sets=3 failure_cases=2") + + +if __name__ == "__main__": + main() diff --git a/scripts/extract_juno_v1_tx_sets.py b/scripts/extract_juno_v1_tx_sets.py new file mode 100644 index 000000000..334b77129 --- /dev/null +++ b/scripts/extract_juno_v1_tx_sets.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Extract Astroport-Juno v1 deployment --set values from junod tx JSON. + +Use this after `junod tx wasm store ... -o json` or `junod tx wasm instantiate ... -o json` +outputs are available. Map each tx JSON file to the deployment config key it +should populate, and the script prints copy/paste flags for +`scripts/fill_juno_v1_deployment_config.py`. + +Examples: + python3 scripts/extract_juno_v1_tx_sets.py \ + --code-id astroport-factory=store-factory.json \ + --address astroport-factory=instantiate-factory.json + + python3 scripts/extract_juno_v1_tx_sets.py --scan store-factory.json instantiate-factory.json +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +from collections.abc import Iterable +from typing import Any, NoReturn + +CODE_ID_KEYS = { + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-pair", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", + "cw20-base", +} + +ADDRESS_KEYS = { + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +} + +CODE_ATTRS = {"code_id", "codeID", "code-id"} +ADDRESS_ATTRS = {"_contract_address", "contract_address", "contract-address"} + + +def fail(message: str) -> NoReturn: + print(f"FAIL: {message}", file=sys.stderr) + sys.exit(1) + + +def load_json(path: pathlib.Path) -> Any: + try: + return json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing tx JSON: {path}") + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path}: {exc}") + + +def iter_events(value: Any) -> Iterable[dict[str, Any]]: + """Yield Cosmos SDK event objects from common tx response shapes.""" + if isinstance(value, dict): + events = value.get("events") + if isinstance(events, list): + for event in events: + if isinstance(event, dict): + yield event + + tx_response = value.get("tx_response") + if isinstance(tx_response, dict): + yield from iter_events(tx_response) + + logs = value.get("logs") + if isinstance(logs, list): + for log in logs: + if isinstance(log, dict): + yield from iter_events(log) + + raw_log = value.get("raw_log") + if isinstance(raw_log, str) and raw_log.startswith("["): + try: + yield from iter_events({"logs": json.loads(raw_log)}) + except json.JSONDecodeError: + pass + + +def iter_attributes(event: dict[str, Any]) -> Iterable[tuple[str, str]]: + attrs = event.get("attributes") + if not isinstance(attrs, list): + return + for attr in attrs: + if not isinstance(attr, dict): + continue + key = attr.get("key") + value = attr.get("value") + if isinstance(key, str) and isinstance(value, str): + yield key, value + + +def extract_values(path: pathlib.Path, wanted_attrs: set[str]) -> list[str]: + data = load_json(path) + values: list[str] = [] + for event in iter_events(data): + for key, value in iter_attributes(event): + if key in wanted_attrs: + values.append(value) + return values + + +def parse_mapping(raw: str, allowed_keys: set[str], kind: str) -> tuple[str, pathlib.Path]: + if "=" not in raw: + fail(f"--{kind} must be NAME=PATH, got {raw!r}") + key, path = raw.split("=", 1) + if key not in allowed_keys: + fail(f"unknown {kind} key {key!r}; expected one of: {', '.join(sorted(allowed_keys))}") + if not path: + fail(f"--{kind} path is empty for {key}") + return key, pathlib.Path(path) + + +def unique_single(values: list[str], path: pathlib.Path, kind: str) -> str: + unique = sorted(set(values)) + if not unique: + fail(f"no {kind} found in {path}") + if len(unique) > 1: + fail(f"multiple {kind}s found in {path}: {', '.join(unique)}; split tx files or use --scan") + return unique[0] + + +def shell_quote(value: str) -> str: + return "'" + value.replace("'", "'\\''") + "'" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--code-id", action="append", default=[], metavar="NAME=PATH", help="map first unique store code_id in PATH to code_ids.NAME") + parser.add_argument("--address", action="append", default=[], metavar="NAME=PATH", help="map first unique instantiate contract address in PATH to addresses.NAME") + parser.add_argument("--scan", nargs="*", type=pathlib.Path, default=[], help="print discovered code IDs and contract addresses without mapping") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + emitted = 0 + + for raw in args.code_id: + key, path = parse_mapping(raw, CODE_ID_KEYS, "code-id") + value = unique_single(extract_values(path, CODE_ATTRS), path, "code_id") + if not value.isdigit(): + fail(f"code_id for {key} in {path} is not numeric: {value!r}") + print(f"--set code_ids.{key}={shell_quote(value)}") + emitted += 1 + + for raw in args.address: + key, path = parse_mapping(raw, ADDRESS_KEYS, "address") + value = unique_single(extract_values(path, ADDRESS_ATTRS), path, "contract address") + print(f"--set addresses.{key}={shell_quote(value)}") + emitted += 1 + + for path in args.scan: + code_ids = sorted(set(extract_values(path, CODE_ATTRS))) + addresses = sorted(set(extract_values(path, ADDRESS_ATTRS))) + print(f"# {path}") + print(f"code_ids={','.join(code_ids) if code_ids else '-'}") + print(f"addresses={','.join(addresses) if addresses else '-'}") + emitted += 1 + + if emitted == 0: + fail("provide at least one --code-id, --address, or --scan input") + + +if __name__ == "__main__": + main() diff --git a/scripts/fill_juno_v1_deployment_config.py b/scripts/fill_juno_v1_deployment_config.py new file mode 100644 index 000000000..8b3918822 --- /dev/null +++ b/scripts/fill_juno_v1_deployment_config.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Render a concrete Astroport-Juno v1 deployment config from the template. + +The template intentionally carries placeholders and zero code IDs. This script is +for the handoff point after uni-7 uploads/instantiates: provide the real values +with repeated --set dotted.path=value flags, and it rewires dependent instantiate +fields so the frontend/deployment config stays internally consistent. Factory +instantiate messages intentionally keep generator_address=null; post_update_state +records the expected factory config after update_config points at incentives. +""" +from __future__ import annotations + +import argparse +import copy +import json +import pathlib +import re +import sys +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DEFAULT_INPUT = ROOT / "deployment" / "juno-v1-testnet.template.json" + +INT_PATHS = { + "code_ids.astroport-factory", + "code_ids.astroport-incentives", + "code_ids.astroport-native-coin-registry", + "code_ids.astroport-oracle", + "code_ids.astroport-pair", + "code_ids.astroport-router", + "code_ids.astroport-tokenfactory-tracker", + "code_ids.astroport-whitelist", + "code_ids.cw20-base", +} + +ADDRESS_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +) + +PLACEHOLDER_RE = re.compile(r"replace|REPLACE", re.IGNORECASE) +LEGACY_INCENTIVES_KEYS = {"astro_token", "vesting_contract"} + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}", file=sys.stderr) + sys.exit(1) + + +def load_json(path: pathlib.Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing input: {path}") + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path}: {exc}") + if not isinstance(data, dict): + fail("top-level config must be a JSON object") + return data + + +def parse_value(path: str, value: str) -> Any: + if path in INT_PATHS: + try: + parsed = int(value) + except ValueError: + fail(f"{path} must be an integer code ID, got {value!r}") + if parsed < 0: + fail(f"{path} must be non-negative") + return parsed + if value in {"true", "false"}: + return value == "true" + if value == "null": + return None + return value + + +def set_path(cfg: dict[str, Any], assignment: str) -> None: + if "=" not in assignment: + fail(f"--set must be dotted.path=value, got {assignment!r}") + path, raw_value = assignment.split("=", 1) + parts = path.split(".") + if not parts or any(part == "" for part in parts): + fail(f"invalid dotted path: {path!r}") + cursor: Any = cfg + for part in parts[:-1]: + if isinstance(cursor, dict) and part in cursor: + cursor = cursor[part] + elif isinstance(cursor, list) and part.isdigit() and int(part) < len(cursor): + cursor = cursor[int(part)] + else: + fail(f"unknown config path: {path!r}") + leaf = parts[-1] + if isinstance(cursor, dict) and leaf in cursor: + cursor[leaf] = parse_value(path, raw_value) + elif isinstance(cursor, list) and leaf.isdigit() and int(leaf) < len(cursor): + cursor[int(leaf)] = parse_value(path, raw_value) + else: + fail(f"unknown config path: {path!r}") + + +def require(cfg: dict[str, Any], *parts: str) -> Any: + cursor: Any = cfg + for part in parts: + if not isinstance(cursor, dict) or part not in cursor: + fail(f"missing expected config path: {'.'.join(parts)}") + cursor = cursor[part] + return cursor + + +def rewire(cfg: dict[str, Any]) -> None: + """Propagate top-level accounts/code IDs/addresses into instantiate messages.""" + accounts = require(cfg, "accounts") + code_ids = require(cfg, "code_ids") + addresses = require(cfg, "addresses") + network = require(cfg, "network") + msgs = require(cfg, "instantiate_msgs") + post_update_state = require(cfg, "post_update_state") + + owner = accounts["owner"] + guardian = accounts["guardian"] + treasury = accounts["treasury"] + tokenfactory_module = accounts["tokenfactory_module"] + native_denom = network["native_asset_denom"] + + msgs["astroport-native-coin-registry"]["owner"] = owner + msgs["astroport-whitelist"]["admins"] = [owner] + + tracker = msgs["astroport-tokenfactory-tracker"] + tracker["tokenfactory_module_address"] = tokenfactory_module + tracker["tracked_denom"] = f"factory/{addresses['astroport-factory']}/astroport/share" + + factory = msgs["astroport-factory"] + factory["coin_registry_address"] = addresses["astroport-native-coin-registry"] + factory["fee_address"] = treasury + factory["generator_address"] = None + factory["owner"] = owner + factory["token_code_id"] = code_ids["cw20-base"] + factory["tracker_config"]["code_id"] = code_ids["astroport-tokenfactory-tracker"] + factory["tracker_config"]["token_factory_addr"] = tokenfactory_module + factory["whitelist_code_id"] = code_ids["astroport-whitelist"] + if len(factory["pair_configs"]) != 1: + fail("factory pair_configs must contain exactly one XYK config before rendering") + factory["pair_configs"][0]["code_id"] = code_ids["astroport-pair"] + + msgs["astroport-router"]["astroport_factory"] = addresses["astroport-factory"] + + incentives = msgs["astroport-incentives"] + legacy = sorted(LEGACY_INCENTIVES_KEYS & incentives.keys()) + if legacy: + fail("instantiate_msgs.astroport-incentives uses legacy key(s): " + ", ".join(legacy)) + incentives["reward_token"] = {"native_token": {"denom": native_denom}} + incentives["factory"] = addresses["astroport-factory"] + incentives["guardian"] = guardian + incentives["owner"] = owner + + factory_final = post_update_state["astroport-factory"] + factory_final["generator_address"] = addresses["astroport-incentives"] + factory_final["pair_configs"] = copy.deepcopy(factory["pair_configs"]) + factory_final["pair_configs"][0]["permissioned"] = False + + oracle = msgs["astroport-oracle"] + oracle["asset_infos"] = [{"native_token": {"denom": native_denom}}] + oracle["factory_contract"] = addresses["astroport-factory"] + + pair_create = cfg["pair_create_msg_template"] + pair_create["asset_infos"][0] = {"native_token": {"denom": native_denom}} + + +def walk_strings(value: Any, path: str = "") -> list[tuple[str, str]]: + found: list[tuple[str, str]] = [] + if isinstance(value, str): + found.append((path, value)) + elif isinstance(value, dict): + for key, item in value.items(): + found.extend(walk_strings(item, f"{path}.{key}" if path else str(key))) + elif isinstance(value, list): + for idx, item in enumerate(value): + found.extend(walk_strings(item, f"{path}[{idx}]")) + return found + + +def assert_complete(cfg: dict[str, Any]) -> None: + zero_ids = [key for key, value in cfg["code_ids"].items() if value == 0] + if zero_ids: + fail(f"code IDs still zero: {', '.join(sorted(zero_ids))}") + placeholders = [(path, value) for path, value in walk_strings(cfg) if PLACEHOLDER_RE.search(value)] + if placeholders: + sample = ", ".join(f"{path}={value!r}" for path, value in placeholders[:5]) + fail(f"placeholder values remain: {sample}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", type=pathlib.Path, default=DEFAULT_INPUT) + parser.add_argument("--output", type=pathlib.Path, required=True) + parser.add_argument("--set", dest="sets", action="append", default=[], help="dotted.path=value override; repeatable") + parser.add_argument("--require-complete", action="store_true", help="fail if any code ID is 0 or placeholder string remains") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + cfg = copy.deepcopy(load_json(args.input)) + for assignment in args.sets: + set_path(cfg, assignment) + rewire(cfg) + if args.require_complete: + assert_complete(cfg) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(cfg, indent=2, sort_keys=False) + "\n") + print(f"OK: wrote rendered Juno v1 deployment config to {args.output}") + print(f"sets={len(args.sets)} require_complete={args.require_complete}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_juno_v1_dry_run_txs.py b/scripts/generate_juno_v1_dry_run_txs.py new file mode 100755 index 000000000..4268acf9e --- /dev/null +++ b/scripts/generate_juno_v1_dry_run_txs.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Generate fake uni-7 tx JSON files for the Astroport-Juno v1 handoff. + +This is for operator rehearsal only. It creates the same 16 filenames named by +`deployment/operator-tx-checklist.md`, with harmless synthetic `code_id` and +`_contract_address` events that `scripts/extract_juno_v1_tx_sets.py` can parse. +It never touches real chain state and should not be used as deployment evidence. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +from typing import Any + +STORE_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-pair", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", + "cw20-base", +) + +ADDRESS_KEYS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +) + + +def event_tx(event_type: str, attrs: dict[str, str], memo: str) -> dict[str, Any]: + """Return a minimal Cosmos SDK tx-response-shaped JSON object.""" + return { + "height": "0", + "txhash": f"DRYRUN_{memo.upper().replace('-', '_')}", + "code": 0, + "raw_log": "[]", + "tx_response": { + "code": 0, + "events": [ + { + "type": event_type, + "attributes": [{"key": key, "value": value} for key, value in attrs.items()], + } + ], + }, + } + + +def synthetic_address(index: int, key: str) -> str: + # Deliberately not a real address; enough shape for config plumbing tests. + stem = key.replace("astroport-", "").replace("-", "")[:18] + return f"juno1dryrun{index:02d}{stem}000000000000000000" + + +def write_json(path: pathlib.Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=False) + "\n") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--output-dir", + type=pathlib.Path, + default=pathlib.Path("deployment/tx/uni-7-dry-run"), + help="directory to write synthetic tx JSON files into", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + output_dir: pathlib.Path = args.output_dir + + written: list[pathlib.Path] = [] + for offset, key in enumerate(STORE_KEYS, start=1): + path = output_dir / f"store-{key}.json" + write_json(path, event_tx("store_code", {"code_id": str(7000 + offset)}, f"store-{key}")) + written.append(path) + + for offset, key in enumerate(ADDRESS_KEYS, start=1): + path = output_dir / f"instantiate-{key}.json" + write_json( + path, + event_tx("instantiate", {"_contract_address": synthetic_address(offset, key)}, f"instantiate-{key}"), + ) + written.append(path) + + print(f"OK: wrote synthetic Astroport-Juno v1 tx JSON files to {output_dir}") + print(f"store_txs={len(STORE_KEYS)} instantiate_txs={len(ADDRESS_KEYS)} total={len(written)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_juno_v1_frontend_types.py b/scripts/generate_juno_v1_frontend_types.py new file mode 100644 index 000000000..c7e16337a --- /dev/null +++ b/scripts/generate_juno_v1_frontend_types.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Generate the TypeScript handoff type for Astroport-Juno v1 frontend config. + +The committed deployment template is the source of truth for frontend-facing keys. +This generator emits a narrow declaration file that frontend code can import or +copy without learning every deployment-only field. It intentionally models only +v1 XYK launch scope: factory/router/registry/incentives plus optional oracle; +no prelaunch hardcoded pools, no DEX token, no stable/PCL surfaces. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +from typing import Any, NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DEFAULT_TEMPLATE = ROOT / "deployment" / "juno-v1-testnet.template.json" +DEFAULT_OUTPUT = ROOT / "deployment" / "juno-v1-frontend-config.d.ts" + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}") + sys.exit(1) + + +def load_template(path: pathlib.Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text()) + except FileNotFoundError: + fail(f"missing deployment template: {path}") + except json.JSONDecodeError as exc: + fail(f"invalid deployment template JSON: {exc}") + if not isinstance(data, dict): + fail("deployment template must be a JSON object") + return data + + +def require_string_list(cfg: dict[str, Any], path: str) -> list[str]: + cur: Any = cfg + for part in path.split("."): + if not isinstance(cur, dict) or part not in cur: + fail(f"missing {path}") + cur = cur[part] + if not isinstance(cur, list) or not all(isinstance(item, str) for item in cur): + fail(f"{path} must be a string list") + return cur + + +def require_dict(cfg: dict[str, Any], key: str) -> dict[str, Any]: + value = cfg.get(key) + if not isinstance(value, dict): + fail(f"{key} must be an object") + return value + + +def ts_union(values: list[str]) -> str: + return " | ".join(json.dumps(value) for value in values) + + +def render(cfg: dict[str, Any]) -> str: + network = require_dict(cfg, "network") + addresses = require_dict(cfg, "addresses") + code_ids = require_dict(cfg, "code_ids") + required = require_string_list(cfg, "frontend.required_addresses") + optional = require_string_list(cfg, "frontend.optional_addresses") + + all_frontend = required + optional + missing = [key for key in all_frontend if key not in addresses] + if missing: + fail(f"frontend keys missing from addresses: {missing}") + + if "pools" in require_dict(cfg, "frontend") or "pairs" in require_dict(cfg, "frontend"): + fail("frontend template must not hardcode pools/pairs") + + pair_template = require_dict(cfg, "pair_create_msg_template") + pair_type = pair_template.get("pair_type") + if not isinstance(pair_type, dict) or set(pair_type) != {"xyk"}: + fail("pair_create_msg_template must stay XYK-only") + + code_keys = sorted(str(key) for key in code_ids) + address_keys = sorted(str(key) for key in addresses) + + native = network.get("native_asset_denom") + if not isinstance(native, str) or not native: + fail("network.native_asset_denom must be a non-empty string") + + return f'''// AUTO-GENERATED by scripts/generate_juno_v1_frontend_types.py; do not edit by hand. +// Source: deployment/juno-v1-testnet.template.json + +export type JunoV1CodeIdKey = {ts_union(code_keys)}; + +export type JunoV1AddressKey = {ts_union(address_keys)}; + +export type JunoV1RequiredFrontendAddressKey = {ts_union(required)}; + +export type JunoV1OptionalFrontendAddressKey = {ts_union(optional)}; + +export type JunoV1FrontendAddressKey = + | JunoV1RequiredFrontendAddressKey + | JunoV1OptionalFrontendAddressKey; + +export type NativeAssetInfo = {{ native_token: {{ denom: string }} }}; +export type XykPairType = {{ xyk: Record }}; + +export interface JunoV1DeploymentNetwork {{ + chain_id: "uni-7" | "juno-1" | string; + bech32_prefix: "juno"; + fee_denom: string; + native_asset_denom: "{native}" | string; +}} + +export interface JunoV1FrontendConfig {{ + required_addresses: JunoV1RequiredFrontendAddressKey[]; + optional_addresses: JunoV1OptionalFrontendAddressKey[]; + pair_discovery: string; +}} + +export interface JunoV1PairCreateMsgTemplate {{ + pair_type: XykPairType; + asset_infos: [NativeAssetInfo, NativeAssetInfo]; + init_params: null; +}} + +export interface JunoV1FrontendDeploymentConfig {{ + network: JunoV1DeploymentNetwork; + code_ids: Record; + addresses: Record; + pair_create_msg_template: JunoV1PairCreateMsgTemplate; + frontend: JunoV1FrontendConfig; +}} +''' + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--template", type=pathlib.Path, default=DEFAULT_TEMPLATE) + parser.add_argument("--output", type=pathlib.Path, default=DEFAULT_OUTPUT) + parser.add_argument("--check", action="store_true", help="fail if output is stale instead of writing it") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + rendered = render(load_template(args.template)) + if args.check: + try: + current = args.output.read_text() + except FileNotFoundError: + fail(f"missing generated frontend type file: {args.output.relative_to(ROOT)}") + if current != rendered: + fail(f"stale generated frontend type file: {args.output.relative_to(ROOT)}") + print("OK: Juno v1 frontend TypeScript handoff matches deployment template") + print(f"output={args.output.relative_to(ROOT)} bytes={len(rendered)}") + return + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered) + print(f"OK: wrote {args.output.relative_to(ROOT)}") + print(f"bytes={len(rendered)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/summarize_juno_v1_schema_surface.py b/scripts/summarize_juno_v1_schema_surface.py new file mode 100755 index 000000000..79e1e3c72 --- /dev/null +++ b/scripts/summarize_juno_v1_schema_surface.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Summarize the committed Astroport-Juno v1 schema surface for frontend work. + +This is intentionally dependency-free. It reads the generated JSON schemas and +prints the top-level instantiate/execute/query/migrate/sudo message variants plus +response schema files per contract. The output is a quick integration map for UI +and deployment wiring without pretending deferred contracts exist. +""" +from __future__ import annotations + +import json +import pathlib +import sys +from collections.abc import Iterable +from typing import NoReturn + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SCHEMAS = ROOT / "schemas" +MESSAGE_FILES = ("instantiate", "execute", "query", "migrate", "sudo") +EXPECTED_CONTRACTS = ( + "astroport-factory", + "astroport-incentives", + "astroport-native-coin-registry", + "astroport-oracle", + "astroport-pair", + "astroport-router", + "astroport-tokenfactory-tracker", + "astroport-whitelist", +) + + +def fail(msg: str) -> NoReturn: + print(f"FAIL: {msg}", file=sys.stderr) + sys.exit(1) + + +def load_json(path: pathlib.Path) -> dict: + try: + return json.loads(path.read_text()) + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") + + +def top_level_variants(schema: dict) -> list[str]: + """Return snake_case top-level message variants from a cosmwasm schema.""" + variants: list[str] = [] + for branch in schema.get("oneOf", []): + required = branch.get("required") or [] + if required: + variants.append(str(required[0])) + if variants: + return variants + # Instantiate schemas are often a single object rather than oneOf. + props = schema.get("properties") or {} + return sorted(str(k) for k in props) + + +def bullet_list(items: Iterable[str]) -> str: + values = list(items) + if not values: + return "—" + return ", ".join(f"`{item}`" for item in values) + + +def main() -> None: + if not SCHEMAS.exists(): + fail("schemas/ directory is missing") + + actual_contracts = sorted(p.name for p in SCHEMAS.iterdir() if p.is_dir()) + missing = sorted(set(EXPECTED_CONTRACTS) - set(actual_contracts)) + extra = sorted(set(actual_contracts) - set(EXPECTED_CONTRACTS)) + if missing or extra: + fail(f"schema contract set mismatch: missing={missing} extra={extra}") + + print("# Astroport-Juno v1 frontend schema surface") + print() + print("Generated from committed `schemas/*/raw/*.json`. Keep this surface boring: XYK swap/liquidity, pair discovery, registry/oracle reads, and external incentives only.") + print() + print("| Contract | Instantiate fields | Execute variants | Query variants | Other messages | Response schemas |") + print("|---|---|---|---|---|---|") + + for contract in EXPECTED_CONTRACTS: + raw = SCHEMAS / contract / "raw" + if not raw.exists(): + fail(f"missing raw schema dir for {contract}") + + columns: dict[str, list[str]] = {} + other: list[str] = [] + for message in MESSAGE_FILES: + path = raw / f"{message}.json" + if not path.exists(): + columns[message] = [] + continue + variants = top_level_variants(load_json(path)) + if message in ("instantiate", "execute", "query"): + columns[message] = variants + else: + other.extend(f"{message}:{variant}" for variant in variants) + + responses = sorted(p.stem.removeprefix("response_to_") for p in raw.glob("response_to_*.json")) + print( + f"| `{contract}` | {bullet_list(columns.get('instantiate', []))} | " + f"{bullet_list(columns.get('execute', []))} | {bullet_list(columns.get('query', []))} | " + f"{bullet_list(other)} | {bullet_list(responses)} |" + ) + + print() + print(f"contracts={len(EXPECTED_CONTRACTS)}") + + +if __name__ == "__main__": + main() diff --git a/skills/juno-dex-trading/SKILL.md b/skills/juno-dex-trading/SKILL.md new file mode 100644 index 000000000..23bf2f631 --- /dev/null +++ b/skills/juno-dex-trading/SKILL.md @@ -0,0 +1,56 @@ +--- +name: juno-dex-trading +description: Plan, verify, execute, and monitor swaps and liquidity actions on the Juno DEX using live pool, route, wallet, and indexer data. Use for Juno DEX trading, token swaps, route quotes, slippage checks, liquidity management, LP staking, reward claims, transaction review, or Juno asset and contract verification. +--- + +# Trade on Juno DEX + +Use live Juno mainnet data to prepare and, only when explicitly authorized, execute swaps and liquidity transactions. Treat curated metadata as identity information, not proof of price or liquidity. + +## Follow the trading workflow + +1. Confirm the requested action, assets, amount, and acceptable slippage. Distinguish an informational quote from authorization to broadcast. +2. Read the current deployment and asset metadata from `frontend/src/data/registry.juno-1.json`. Do not copy contract addresses from memory. +3. Confirm the connected chain is `juno-1`. Keep read-only analysis available when no wallet is connected, but do not construct a claim of execution. +4. Resolve each ticker to its full native denom, TokenFactory denom, IBC hash, or CW20 address. Show the ticker to the user and retain the full identifier for verification and message construction. +5. Query current pools and simulate the route immediately before execution. Prefer the best live route; never invent reserves, prices, candles, volume, APR, or recent activity. +6. Normalize display amounts with each asset's configured decimals. Keep base-unit integers for contract messages. Treat `ujuno` as 6-decimal JUNO. +7. Report expected output, minimum received, route hops, fees, price impact when supported, and material asset or liquidity risks. Require explicit acknowledgement for unverified assets or routes. +8. Requote if the amount, assets, route, slippage, or quote age changes. Disable execution while route simulation is pending or unavailable. +9. Broadcast only after explicit user authorization. Respect wallet rejection as final; do not retry or increase slippage automatically. +10. Return the transaction hash, explorer link, and final chain result. Refresh balances, pool state, quote, and indexed activity after success. + +## Apply safety rules + +- Never request, expose, store, or transmit a seed phrase or private key. +- Never infer trade authorization from a request to research, quote, explain, diagnose, or review. +- Never substitute a similarly named token. Compare the complete denom or contract address. +- Never silently route through an unverified or thin-liquidity pool. +- Never use stale or placeholder market figures when live data is absent. State that the data is unavailable. +- Never convert a wallet rejection into an automatic retry. +- Never claim success from broadcast intent alone. Verify the returned transaction result. + +## Use repository-native interfaces + +- Use `frontend/src/lib/astroport/routes.ts` and `frontend/src/queries/useSwapQuote.ts` for route construction and simulation behavior. +- Use `frontend/src/lib/astroport/messages.ts` for direct pair swaps and liquidity messages. +- Use `frontend/src/lib/indexer/client.ts` for candles, metrics, positions, and activity. +- Use `frontend/src/lib/format/amounts.ts` for base/display unit conversion. +- Use `frontend/src/lib/risk.ts` for verification and route-risk policy. +- Use `frontend/src/tx/useTxRunner.tsx` for transaction lifecycle, error decoding, and post-success invalidation. + +Inspect these files at execution time because deployment addresses, enabled pools, and application behavior can change. + +## Present a trade review + +Before an authorized broadcast, summarize: + +- network and wallet readiness; +- offered and requested assets with full identifiers; +- display amount and base-unit amount; +- route and pool addresses; +- expected and minimum received; +- max slippage and known price impact; +- warnings requiring acknowledgement. + +After broadcast, summarize the confirmed status and provide a copyable transaction hash and explorer link. diff --git a/skills/juno-dex-trading/agents/openai.yaml b/skills/juno-dex-trading/agents/openai.yaml new file mode 100644 index 000000000..b018d682e --- /dev/null +++ b/skills/juno-dex-trading/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Juno DEX Trading" + short_description: "Trade safely on Juno with verified live data" + default_prompt: "Use $juno-dex-trading to plan or execute a safe Juno DEX trade using current pool, quote, and wallet data."