diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 00000000000..454b8427cd7 --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +../.claude/skills \ No newline at end of file diff --git a/.claude/skills/qabot/SKILL.md b/.claude/skills/qabot/SKILL.md index 0f39277af4e..d9c8b23fea8 100644 --- a/.claude/skills/qabot/SKILL.md +++ b/.claude/skills/qabot/SKILL.md @@ -90,6 +90,27 @@ On first visit to any origin (gome.shapeshift.com, release.shapeshift.com, etc.) **IMPORTANT**: Always use the `qabot` profile. The native wallet is stored in this profile's IndexedDB per-origin. +Use a shell-scoped command alias at session start to reduce command noise: + +```bash +AB='agent-browser --session qabot --profile ~/.agent-browser/profiles/qabot' +``` + +Then use `$AB` for all commands in that shell session: + +```bash +$AB open +$AB snapshot +$AB click "Connect Wallet" +$AB screenshot /tmp/step-0.png +``` + +When you need a headed run, append `--headed` only for that command: + +```bash +$AB --headed open +``` + ```bash agent-browser --session qabot --profile ~/.agent-browser/profiles/qabot open ``` @@ -127,6 +148,19 @@ The native wallet requires a password on each session start. The wallet-health f `eval "$(cat /tmp/click-next.js)"` 7. Wait 8+ seconds for external origins to fully hydrate +### PR Review Reliability Checklist (localhost) + +When using qabot for PR review validation on localhost: + +1. Follow PR `Testing` steps verbatim before adding extra assertions. +2. Do a manual-first pass with `agent-browser` in the same live session: + - reach exact page/state + - confirm account/wallet assumptions + - validate selectors and click-path before reporting +3. Keep wallet setup as preflight only (never as reported qabot steps), but ensure required preconditions are visible before step 1 (e.g. `Send` button). +4. Only after manual flow is stable, create/report qabot run steps. +5. If automation friction is selector-related, add/ask for precise `data-testid` at the failing UI control. + ### Tips #### JS Eval & Smart Quotes (CRITICAL) diff --git a/.npmrc b/.npmrc index 8a884f1242b..c0aaed42049 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,4 @@ node-linker=hoisted shamefully-hoist=true -strict-peer-dependencies=false +strict-peer-dependencies=true auto-install-peers=true diff --git a/.railwayignore b/.railwayignore new file mode 100644 index 00000000000..bb091a643f1 --- /dev/null +++ b/.railwayignore @@ -0,0 +1,49 @@ +# Railway snapshot ignore - reduce repo snapshot size for public-api builds +# The Dockerfile.dockerignore handles Docker build context separately; +# this file reduces what Railway snapshots from GitHub before the build starts. + +# Frontend source (not needed for public-api server build) +src/ +cypress/ +e2e/ + +# Large generated/static assets already copied explicitly in Dockerfile +public/generated/generatedAssetData.json.gz +public/generated/generatedAssetData.json.br +public/generated/relatedAssetIndex.json + +# Images and static assets +src/assets/ +*.png +*.jpg +*.jpeg +*.svg +*.gif +*.ico +*.webp + +# Development/IDE files +.vscode/ +.idea/ +.claude/ +.agents/ +.beads/ +.playwright-mcp/ +coverage/ +*.log +.DS_Store + +# Yarn state (not needed for pnpm builds) +.yarn/ + +# Documentation +*.md +!packages/public-api/*.md +!packages/swap-widget/*.md +docs/ + +# Test files +**/*.test.ts +**/*.test.tsx +**/*.spec.ts +**/*.spec.tsx diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz new file mode 100644 index 00000000000..2172ae8136f Binary files /dev/null and b/.yarn/install-state.gz differ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index df7a4af984b..00000000000 --- a/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Agent Instructions - -This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. - -## Quick Reference - -```bash -bd ready # Find available work -bd show # View issue details -bd update --status in_progress # Claim work -bd close # Complete work -bd sync # Sync with git -``` - -## Landing the Plane (Session Completion) - -**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. - -**MANDATORY WORKFLOW:** - -1. **File issues for remaining work** - Create issues for anything that needs follow-up -2. **Run quality gates** (if code changed) - Tests, linters, builds -3. **Update issue status** - Close finished work, update in-progress items -4. **PUSH TO REMOTE** - This is MANDATORY: - ```bash - git pull --rebase - bd sync - git push - git status # MUST show "up to date with origin" - ``` -5. **Clean up** - Clear stashes, prune remote branches -6. **Verify** - All changes committed AND pushed -7. **Hand off** - Provide context for next session - -**CRITICAL RULES:** -- Work is NOT complete until `git push` succeeds -- NEVER stop before pushing - that leaves work stranded locally -- NEVER say "ready to push when you are" - YOU must push -- If push fails, resolve and retry until it succeeds - diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000000..681311eb9cf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index b758b611307..9d96b91f5b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,3 +190,41 @@ Current contracts: - Each wallet has unique `walletId` (e.g., `metamask:0x123`, `ledger:ABC`) - Portfolio state is filtered by active `walletId` - Account discovery runs per wallet on connection + +### Issue Tracking (beads) + +This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. + +```bash +bd ready # Find available work +bd show # View issue details +bd update --status in_progress # Claim work +bd close # Complete work +bd sync # Sync with git +``` + +### Session Completion + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd sync + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds diff --git a/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml b/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml new file mode 100644 index 00000000000..8e1c702969e --- /dev/null +++ b/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml @@ -0,0 +1,27 @@ +name: Chainflip Lending Action Center PR 12064 +description: > + Evidence-only fixture for PR 12064. Assumes the current browser session + already executed Chainflip lending USDC operations and the Action Center + contains the resulting cards. Focuses only on notification center copy, + expanded details, and egress transaction affordances. +route: /#/chainflip-lending/pool/eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 +steps: + - name: Verify confirmed Chainflip lending cards + instruction: > + Open the notification center drawer and verify the confirmed Chainflip + lending cards for deposit, borrow, repay, add collateral, and remove + collateral show sane user copy with expanded details for operation, + amount, and transaction id. + expected: > + Confirmed Chainflip lending cards are visible with expanded details and + no untranslated keys. + screenshot: true + - name: Verify egress card transaction affordance + instruction: > + Verify the egress card shows the withdraw-from-Chainflip copy, expanded + details include the egress transaction id, and a View Transaction button + is present for the external explorer link. + expected: > + The egress Action Center card shows human copy, egress tx id, and a View + Transaction button. + screenshot: true diff --git a/e2e/fixtures/chainflip-lending-revamp-ui.yaml b/e2e/fixtures/chainflip-lending-revamp-ui.yaml new file mode 100644 index 00000000000..29409da8389 --- /dev/null +++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml @@ -0,0 +1,32 @@ +name: Chainflip Lending Revamp UI +description: Validates the revamped Chainflip lending surfaces for USDC pool and key action modals. +route: /chainflip-lending +steps: + - name: Chainflip lending dashboard + instruction: Open the chainflip lending dashboard and ensure the refreshed layout is visible. + expected: Chainflip lending dashboard cards and market tables are visible. + screenshot: true + - name: Open usdc pool + instruction: Navigate into the USDC pool from the All Markets table. + expected: USDC pool page is visible with action tabs. + screenshot: true + - name: Supply modal + instruction: Open Supply action and verify supply input controls. + expected: Supply modal is open with amount input, max and submit controls. + screenshot: true + - name: Deposit and egress modal + instruction: Open Deposit to Chainflip tab and open Withdraw modal. + expected: Egress modal is open with destination toggle and amount controls. + screenshot: true + - name: Collateral modal ltv gauge + instruction: Open Collateral tab and open Add Collateral modal. + expected: LTV gauge shows target, soft liquidation and hard liquidation labels without overlap. + screenshot: true + - name: Borrow modal + instruction: Open Manage Loan tab and open Borrow modal. + expected: Borrow modal is open with amount input and target LTV section. + screenshot: true + - name: Repay modal + instruction: Open Repay modal from Manage Loan tab. + expected: Repay modal is open with full repayment toggle and amount controls. + screenshot: true diff --git a/e2e/fixtures/chainflip-lending.yaml b/e2e/fixtures/chainflip-lending.yaml new file mode 100644 index 00000000000..7e1740c38e9 --- /dev/null +++ b/e2e/fixtures/chainflip-lending.yaml @@ -0,0 +1,661 @@ +name: Chainflip Lending +description: > + End-to-end round-trip across all 3 fund buckets on the USDC pool. + Assumes the user has an EXISTING Chainflip Lending position (account + already registered, refund address set, prior deposits exist). + + Round-trip sequence: + Navigate → Deposit ($0.50-$2) → Supply → Withdraw Supply → + Add Collateral → Borrow (small, safe LTV) → Repay → + Remove Collateral → Egress + + Each step verifies: + - Toast notification with correct domain language + - Action Center card with correct status + operation type + + === THREE FUND BUCKETS === + + All on Chainflip State Chain: + - Free balance: staging area. Deposits land here. + - Supply: allocated to lending pools, earning yield. + - Collateral: backing loans, determines borrowing power. + + Funds MUST move through free balance between supply and collateral. + + === TRANSACTION FLOW === + + All State Chain ops use EIP-712 signing: + 1. App SCALE-encodes the call + 2. User signs structured typed data in wallet + 3. App submits signed extrinsic to State Chain + 4. App polls State Chain for confirmation (~6s intervals) + + Deposit is different: EIP-712 to open channel → EVM tx to channel + address → State Chain witnesses the deposit. + + === TIMING === + + - EVM on-chain tx (deposit send): 30-60s confirmation + - State Chain confirmation polling: 5-10s per confirmation + - Egress to wallet: 60-300s (on-chain broadcast from CF vaults) + - After ANY action button: wait up to 180s total + + === HARD CONSTRAINTS === + + 1. USDC pool only. Single pool round-trip. + 2. Existing position — account is registered, refund address set. + 3. $2 max deposit ceiling. Start at $0.50, increment to $1 then $2 + if amount is below the protocol minimum. + 4. Supply minimum: ~$100 from protocol. If existing free balance is + insufficient, use whatever free balance exists. If under $100, + skip supply with SKIPPED (not passed, not failed). + 5. Borrow: smallest possible amount ($10 minimum). Keep LTV << 80%. + 6. Collateral: $10 minimum for updates. + 7. Skipped steps = SKIPPED with explanation. Never mark skipped as + passed or failed. + 8. No Bitcoin deposits. + 9. No force-funding swaps — work with existing balances. + 10. Report every operation with action, asset, amount, before/after. + + === ACTION CENTER VERIFICATION === + + After EVERY successful flow: + 1. Wait up to 10s for toast notification (bottom-right desktop) + 2. Read toast text — must mention asset and amount + 3. Click toast → Action Center drawer opens + 4. Verify latest card: + - Type label: "Chainflip Lending" + - Status: green "Confirmed" + - Description: correct operation + amount + - Asset icon: USDC + 5. Close Action Center + + If toast missing within 10s: SOFT FAIL, verify Action Center + manually via bell icon. + + === NATIVE WALLET === + + At each signing step, a "Confirm" button may appear for hardware + wallets. Click it. If password prompt: enter $NATIVE_WALLET_PASSWORD. + +route: /chainflip-lending +depends_on: + - wallet-health.yaml + +steps: + # ======================================================================== + # PHASE 0: Navigate and record baseline + # ======================================================================== + + - name: Ensure Chainflip Lending feature flag enabled + instruction: > + eval "window.location.hash = '/'" + Wait 2 seconds. + + Verify the current session has the Chainflip Lending feature enabled + before navigating into the flow. + + If you can inspect Redux state, confirm: + preferences.featureFlags.ChainflipLending === true + + If the Chainflip Lending route/header is unavailable in this session, + mark this scenario SKIPPED with note: + "ChainflipLending feature flag is disabled for this session." + expected: > + Chainflip Lending is enabled for this session before flow validation starts. + screenshot: true + + - name: Navigate to Chainflip Lending + instruction: > + eval "window.location.hash = '/chainflip-lending'" + Wait 5 seconds. + + Verify: + 1. Page loads with "Chainflip Lending" header + 2. Markets table shows pools (BTC, ETH, SOL, USDC, USDT) + 3. Stats cards show Total Supplied, Available Liquidity, + Total Borrowed (all > $0) + expected: > + Chainflip Lending page loads with live pool data. + screenshot: true + + - name: Navigate to USDC pool and record baseline + instruction: > + Click USDC row in markets table, or: + eval "window.location.hash = '/chainflip-lending/pool/usdc'" + Wait 5 seconds. + + Record baseline "Your Position" values exactly: + - Free Balance: ___ USDC + - Supplied: ___ USDC + - Collateral: ___ USDC + - Any active loans? Amount: ___ + + Also record pool stats: supply APY, borrow rate, utilisation. + + These baselines are critical for verifying each subsequent step. + expected: > + USDC pool page loads. Baseline position recorded. + screenshot: true + + # ======================================================================== + # PHASE 1: Deposit (wallet → free balance) + # $0.50 start, increment if below minimum. $2 absolute max. + # ======================================================================== + + - name: Deposit USDC > Open modal + instruction: > + Click the "Deposit" button on the USDC pool page. + Modal opens with amount input. + + Verify: + 1. Title mentions "Deposit" + 2. Available balance shows wallet USDC balance + 3. Current free balance indicator visible + 4. This is a returning user — no refund address step expected + expected: > + Deposit modal opens showing wallet USDC balance. + screenshot: true + + - name: Deposit USDC > Enter amount ($0.50 start) + instruction: > + Enter "0.50" in the amount input. Wait 2 seconds. + + Check for minimum deposit warning: + - If NO warning and submit enabled: proceed with $0.50 + - If minimum warning appears: clear input, try "1.00" + - If still below minimum: clear input, try "2.00" + - If $2.00 is still below minimum: SKIP entire deposit phase + as SKIPPED with note "minimum deposit exceeds $2 ceiling" + + Once a valid amount is accepted, click submit/continue. + Wait for confirm screen. Verify amount shown. + Click "Confirm & Deposit". + expected: > + Valid deposit amount entered ($0.50-$2) and confirmed. + OR SKIPPED if minimum exceeds $2. + screenshot: true + + - name: Deposit USDC > Execute deposit + instruction: > + If previous step was SKIPPED, mark this SKIPPED too. + + The stepper shows progress. For returning users: + - Open Channel (EIP-712 sign) + - Send Deposit (EVM on-chain tx) + - Confirming (State Chain witness) + + For native wallet: click "Confirm" at each signing step. + If password prompt: enter $NATIVE_WALLET_PASSWORD. + + Wait up to 180 seconds. Poll snapshots every 15 seconds. + + Expected: success screen with deposit confirmation. + expected: > + Deposit completes. Success screen shows deposited amount. + screenshot: true + + - name: Deposit USDC > Verify toast + Action Center + instruction: > + If deposit was SKIPPED, mark this SKIPPED too. + + Wait up to 10 seconds for toast notification. + Toast should mention deposit amount and "Chainflip". + + If toast visible: + 1. Record toast text + 2. Click toast → Action Center drawer opens + 3. Verify latest card: + - Type: "Chainflip Lending" + - Status: "Confirmed" (green) + - Mentions deposit and USDC + 4. Close Action Center + + If no toast: open Action Center via bell icon. SOFT FAIL. + expected: > + Toast notification for deposit. Action Center card correct. + screenshot: true + soft_fail: true + + - name: Deposit USDC > Verify free balance increase + instruction: > + If deposit was SKIPPED, mark this SKIPPED too. + + Click "Done". Wait 3 seconds. + + Verify free balance increased by the deposited amount + compared to Phase 0 baseline. + + Record new free balance. + + REPORT: "DEPOSIT: X USDC. Free balance: [before] → [after]." + expected: > + Free balance increased by deposit amount. + screenshot: true + + # ======================================================================== + # PHASE 2: Supply (free balance → lending pool) + # Minimum $100. If free balance < $100, SKIP. + # ======================================================================== + + - name: Supply USDC > Check feasibility + instruction: > + Check current free balance on the USDC pool page. + + If free balance < $100: + SKIP entire supply phase (steps through "Verify position") + with note: "Free balance ($X) below $100 supply minimum. + Cannot supply without larger deposit (exceeds $2 ceiling)." + + If free balance >= $100: + Click "Supply" button. Modal opens. + Verify available balance shows free balance amount. + expected: > + Supply feasible (free balance >= $100) or SKIPPED. + screenshot: true + + - name: Supply USDC > Enter amount and confirm + instruction: > + If previous step was SKIPPED, mark this SKIPPED too. + + Enter "101" in amount input (just above $100 minimum). + If free balance < 101 but >= 100, enter "100" or use Max. + + Verify no minimum warning. Click submit. + Verify confirm screen. Click "Confirm & Supply". + expected: > + Supply amount confirmed. + screenshot: true + + - name: Supply USDC > Execute + instruction: > + If supply was SKIPPED, mark this SKIPPED too. + + Stepper: Signing (EIP-712) → Confirming. + For native wallet: click "Confirm" button. + Wait up to 120 seconds. + + Expected: success screen "Supply Successful". + expected: > + Supply completes successfully. + screenshot: true + + - name: Supply USDC > Verify toast + Action Center + instruction: > + If supply was SKIPPED, mark this SKIPPED too. + + Wait 10s for toast. Should mention supply + USDC + amount. + + If toast visible: + 1. Click toast → Action Center opens + 2. Verify latest card: "Chainflip Lending", "Confirmed", + mentions supply + 3. Close drawer + + If no toast: check Action Center manually. SOFT FAIL. + expected: > + Toast + Action Center card for supply operation. + screenshot: true + soft_fail: true + + - name: Supply USDC > Verify position + instruction: > + If supply was SKIPPED, mark this SKIPPED too. + + Click "Done". Wait 3 seconds. + + Verify: + - Supplied increased by supply amount + - Free balance decreased by supply amount + + REPORT: "SUPPLY: X USDC. Free: [before] → [after]. + Supplied: [before] → [after]." + expected: > + Supplied balance increased. Free balance decreased. + screenshot: true + + # ======================================================================== + # PHASE 3: Withdraw supply (lending pool → free balance) + # Only if supply was executed. + # ======================================================================== + + - name: Withdraw Supply > Open modal + instruction: > + If supply phase was SKIPPED, mark this SKIPPED too. + + Click "Withdraw Supply" or "Withdraw" button. + Modal opens. Verify available shows supply position. + + Do NOT check "Also withdraw to wallet" — we need funds + in free balance for collateral next. + expected: > + Withdraw modal shows supply position as available. + screenshot: true + + - name: Withdraw Supply > Full withdrawal + instruction: > + If withdraw was SKIPPED, mark this SKIPPED too. + + Click "Max" to withdraw the full supply position. + Click submit. Verify confirm screen. Click "Confirm & Withdraw". + + For native wallet: click "Confirm". + Wait up to 120 seconds. + + Expected: success screen "Withdrawal Successful". + expected: > + Full withdrawal completes. + screenshot: true + + - name: Withdraw Supply > Verify toast + Action Center + instruction: > + If withdraw was SKIPPED, mark this SKIPPED too. + + Wait 10s for toast. Should mention withdrawal + USDC. + + If toast visible: + 1. Click toast → Action Center + 2. Verify card: "Chainflip Lending", "Confirmed", withdrawal + 3. Close drawer + + If no toast: SOFT FAIL, check manually. + expected: > + Toast + Action Center card for withdrawal. + screenshot: true + soft_fail: true + + - name: Withdraw Supply > Verify balances + instruction: > + If withdraw was SKIPPED, mark this SKIPPED too. + + Click "Done". Wait 3 seconds. + + Verify: + - Free balance restored (supply amount returned) + - Supplied back to previous level (or ~0 if full withdraw) + + REPORT: "WITHDRAW: X USDC. Supplied: [before] → [after]. + Free: [before] → [after]." + expected: > + Free balance restored. Supply position cleared or reduced. + screenshot: true + + # ======================================================================== + # PHASE 4: Add Collateral (free balance → collateral) + # Minimum $10. If no collateral UI, SKIP phases 4-7. + # ======================================================================== + + - name: Add Collateral > Check availability + instruction: > + On USDC pool page, look for "Collateral" tab, "Add Collateral" + button, or collateral section. + + If NOT present: + SKIP phases 4-7 with note: "Collateral/Borrow flow not + available in this build (PR #12026 not merged)." + + If present and free balance < $10: + SKIP with note: "Free balance below $10 collateral minimum." + + If present and free balance >= $10: proceed. + expected: > + Collateral UI available and free balance sufficient, or SKIPPED. + screenshot: true + + - name: Add Collateral > Enter amount and execute + instruction: > + If SKIPPED, mark this SKIPPED too. + + Click "Add Collateral". Modal opens. + Enter amount: use all available free balance minus $1 buffer, + minimum $10. If free balance is $50, enter "49". + + Click submit. Confirm. Sign (native wallet: click Confirm). + Wait up to 120 seconds. + + Expected: success screen. + expected: > + Collateral added successfully. + screenshot: true + + - name: Add Collateral > Verify toast + balances + instruction: > + If SKIPPED, mark this SKIPPED too. + + Check toast (10s). Verify Action Center card if toast appears. + + After "Done", verify: + - Collateral increased + - Free balance decreased + + REPORT: "ADD COLLATERAL: X USDC. Free: [before] → [after]. + Collateral: [before] → [after]." + expected: > + Collateral balance increased. Action Center card present. + screenshot: true + soft_fail: true + + # ======================================================================== + # PHASE 5: Borrow (against collateral) + # Minimum $10 for loan update, $100 for new loan. + # Keep LTV well below 80%. + # ======================================================================== + + - name: Borrow > Open modal + instruction: > + If collateral phase was SKIPPED, mark this SKIPPED too. + + Look for "Borrow" tab or button. If not present: SKIP. + + Click it. Modal shows: + - Available to borrow (collateral × 80% LTV - existing debt) + - Current LTV + - Amount input + + Enter "10" (minimum for loan update, or $100 if new loan + and available). Use the smallest valid amount. + + If available borrow capacity < $10: SKIP with note + "Insufficient borrowing capacity." + expected: > + Borrow modal open with amount entered. + screenshot: true + soft_fail: true + + - name: Borrow > Execute + instruction: > + If SKIPPED, mark this SKIPPED too. + + Click submit. Confirm. Sign. Wait up to 120 seconds. + + Expected: success screen. Verify LTV shown is low (safe zone). + expected: > + Borrow completes. LTV in safe zone. + screenshot: true + soft_fail: true + + - name: Borrow > Verify toast + balances + instruction: > + If SKIPPED, mark this SKIPPED too. + + Check toast. Verify Action Center card. + + Verify: + - Active loan shows borrowed amount + - Free balance increased (borrowed funds land in free balance) + - LTV < 80% + + REPORT: "BORROW: X USDC. LTV: Y%. Free: [before] → [after]." + expected: > + Loan active. Free balance increased. LTV safe. + screenshot: true + soft_fail: true + + # ======================================================================== + # PHASE 6: Repay (full repayment) + # ======================================================================== + + - name: Repay > Full repayment + instruction: > + If borrow was SKIPPED, mark this SKIPPED too. + + Look for "Repay" tab or button. Click it. + + Toggle "Full Repayment" or click Max. + Verify free balance can cover the full debt. + + Click submit. Confirm. Sign. Wait up to 120 seconds. + + Expected: success screen. No active loans after. + expected: > + Full repayment completes. No active loans. + screenshot: true + soft_fail: true + + - name: Repay > Verify toast + balances + instruction: > + If SKIPPED, mark this SKIPPED too. + + Check toast. Verify Action Center card. + + Verify: + - No active loans + - LTV: 0% or N/A + - Free balance decreased by repayment amount + + REPORT: "REPAY: Full repayment of X USDC. Loans: 0." + expected: > + Loan cleared. Action Center card present. + screenshot: true + soft_fail: true + + # ======================================================================== + # PHASE 7: Remove Collateral (collateral → free balance) + # ======================================================================== + + - name: Remove Collateral > Execute + instruction: > + If collateral phases were SKIPPED, mark this SKIPPED too. + + Look for "Remove Collateral" option. Click it. + Enter full collateral amount or click Max (no loans, safe + to remove everything). + + Submit. Confirm. Sign. Wait up to 120 seconds. + + After success, verify: + - Collateral: 0 (or reduced) + - Free balance increased + + Check toast + Action Center card. + + REPORT: "REMOVE COLLATERAL: X USDC. Collateral: [before] → [after]. + Free: [before] → [after]." + expected: > + Collateral removed. Free balance restored. + screenshot: true + soft_fail: true + + # ======================================================================== + # PHASE 8: Egress (free balance → wallet) + # ======================================================================== + + - name: Egress > Open modal + instruction: > + On USDC pool page, click "Withdraw from Chainflip" or egress + button. + + Verify: + - Available: current free balance + - Destination address pre-filled with wallet address + expected: > + Egress modal opens with free balance and destination. + screenshot: true + + - name: Egress > Withdraw to wallet + instruction: > + Click Max or enter full free balance amount. + Verify destination is wallet address. + + Click submit. Confirm. Click "Confirm Withdrawal". + + For native wallet: click "Confirm". + Wait up to 300 seconds (egress is an on-chain broadcast from + Chainflip vaults — can take 1-5 minutes). + + Expected: success screen with optional tx reference/link. + expected: > + Egress completes. Transaction reference shown. + screenshot: true + + - name: Egress > Verify toast + Action Center + instruction: > + Wait 10s for toast. Should mention withdrawal from Chainflip. + + If toast visible: + 1. Click toast → Action Center + 2. Verify card: "Chainflip Lending", "Confirmed" + 3. Close drawer + + If no toast: SOFT FAIL, check manually. + expected: > + Toast + Action Center card for egress. + screenshot: true + soft_fail: true + + - name: Egress > Verify final state + instruction: > + Click "Done". Wait 5 seconds. + + Verify: + - Free Balance: ~0 + - Supplied: unchanged or 0 + - Collateral: 0 + + REPORT: "EGRESS: X USDC from State Chain to wallet. + All State Chain balances near zero. Round-trip complete." + expected: > + State Chain balances zeroed. Round-trip complete. + screenshot: true + + # ======================================================================== + # PHASE 9: Final Action Center audit + # ======================================================================== + + - name: Action Center > Full audit + instruction: > + Open Action Center (bell icon in header). + + Count all "Chainflip Lending" cards from this session. + Expected cards (reverse chronological, some may be SKIPPED): + + 1. Egress — Confirmed + 2. Remove Collateral — Confirmed (if available) + 3. Repay — Confirmed (if available) + 4. Borrow — Confirmed (if available) + 5. Add Collateral — Confirmed (if available) + 6. Withdraw from pool — Confirmed (if supply was run) + 7. Supply to pool — Confirmed (if supply was run) + 8. Deposit — Confirmed (if deposit was run) + + For each card verify: + - Type: "Chainflip Lending" + - Status: green "Confirmed" + - Description: domain-appropriate (distinguishable operations) + - Asset icon: USDC + - Timestamp: from this session + + BUG HUNT: + - Missing cards? Duplicates? + - Any stuck in "Pending"? + - Can you distinguish deposit vs supply vs withdraw cards? + + Close Action Center. + + REPORT: "ACTION CENTER AUDIT: X/Y expected cards found. + All statuses correct. Operations distinguishable." + expected: > + All executed operations have Action Center cards. + Types and statuses correct. + screenshot: true + soft_fail: true diff --git a/e2e/fixtures/swap-exploration.yaml b/e2e/fixtures/swap-exploration.yaml new file mode 100644 index 00000000000..f5939b91943 --- /dev/null +++ b/e2e/fixtures/swap-exploration.yaml @@ -0,0 +1,134 @@ +name: Cross-Chain Swap Exploration +description: > + Autonomous exploration of swap flows across all first-class EVM chains, + Solana, Cosmos Hub (ATOM), THORChain (RUNE), and Mayachain (CACAO). + + The agent picks random swap pairs across any combination of chains and + directions - same-chain swaps, cross-chain swaps, any asset to any asset. + The goal is to discover broken quotes, failed broadcasts, UI glitches, + and edge cases that scripted fixtures miss. + + IMPORTANT FINANCIAL CONSTRAINTS: + - Maximum $10 per individual swap (use fiat mode, never exceed) + - Maximum $5 CUMULATIVE downside across ALL swaps in the session + (downside = fees + gas + price impact + slippage losses) + - Track running downside: after each swap, compare USD value sent vs received + - If cumulative downside approaches $4, reduce swap sizes to $1-2 + - If cumulative downside hits $5, STOP executing swaps (can still explore UI) + - Prefer small swaps ($1-3) to minimize downside per trade + + CHAIN COVERAGE: + First-class EVM chains: Ethereum (ETH), Avalanche (AVAX), Base (ETH), + Optimism (ETH), Arbitrum (ETH), BNB Smart Chain (BNB), Polygon (POL), + Gnosis (xDAI) + Non-EVM: Solana (SOL), Cosmos Hub (ATOM), THORChain (RUNE), Mayachain (CACAO) + + SWAP COMBINATIONS TO EXPLORE: + - Same-chain: ETH -> USDC on Ethereum, AVAX -> token on Avalanche, etc. + - Cross-chain EVM: ETH (Ethereum) -> ETH (Base), BNB -> AVAX, etc. + - Cross-ecosystem: ETH -> SOL, BTC -> ATOM, RUNE -> ETH, CACAO -> AVAX + - Exotic pairs: ATOM -> SOL, RUNE -> CACAO, xDAI -> POL + - The agent should try diverse combinations, not just obvious ones + + EXECUTION: + - Navigate via hash route: eval "window.location.hash = '/trade'" + - Use data-testid selectors for asset pickers and trade buttons + - Always check balance before swapping (don't swap more than you have) + - Preview every trade before confirming (check the rate/impact) + - If a quote shows >5% price impact, report as finding but don't execute + - If a swap takes >120s, report as finding (slow swap) + - Track which pairs worked and which failed + +mode: exploratory +route: /trade +depends_on: + - wallet-health.yaml + +domain: + routes: + - /trade + actions: + - switch-sell-asset + - switch-buy-asset + - search-assets + - filter-by-chain + - change-amounts + - preview-trade + - confirm-trade + - sign-swap + - toggle-fiat-crypto + +steps: + - name: Initialize swap session in fiat mode + instruction: > + Start on /trade and run toggle-fiat-crypto so all entered amounts are in USD. + Keep fiat mode enabled for the entire run. Initialize downside tracking at $0. + expected: Fiat mode active and cumulative downside tracker initialized + screenshot: true + - name: Select sell and buy assets across chains + instruction: > + Run switch-sell-asset and switch-buy-asset to create diverse pairs across + first-class EVM chains plus Solana, Cosmos Hub, THORChain, and Mayachain. + Use filter-by-chain and search-assets between selections to broaden coverage. + expected: Diverse same-chain and cross-chain pairs are selected successfully + screenshot: true + - name: Validate balance and per-swap budget before quote + instruction: > + Before each quote, check available sell balance and cap swap size to <= $10. + If balance is insufficient, log a finding and switch to another pair instead + of forcing the trade. + expected: No quote attempt exceeds wallet balance or $10 notional + screenshot: true + - name: Quote and preview candidate swap + instruction: > + Run change-amounts and preview-trade. Wait for quote state to settle and + capture quote failures, stuck loading, or malformed route details as findings. + If price impact is greater than 5%, log the finding and skip execution. + expected: Valid quotes can be previewed; >5% impact routes are skipped and reported + screenshot: true + - name: Confirm and sign eligible swaps only + instruction: > + For swaps that pass checks, run confirm-trade then sign-swap. If signing or + broadcast fails, record the failure and continue exploring other pairs. + Report swaps taking longer than 120s as slow swaps. + expected: Eligible swaps execute, and failures/slow swaps are recorded + screenshot: true + - name: Enforce cumulative downside guardrail + instruction: > + After each completed swap, estimate downside (fees + gas + slippage/impact) + and update cumulative downside. If cumulative downside reaches $4, reduce + notional to $1-$2. If cumulative downside reaches or exceeds $5, stop all + further swap executions and continue UI-only exploration. + expected: Swap execution halts once cumulative downside is >= $5 + screenshot: true + - name: Continuous findings capture + instruction: > + Throughout execution, document findings for broken quotes, failed broadcasts, + infinite loading, route errors, and UI rendering glitches. Include pair, + chain context, and reproduction notes for each finding. + expected: Findings list includes actionable details for each discovered issue + screenshot: true + +constraints: + - Maximum $10 per individual swap + - Maximum $5 cumulative downside (fees + gas + slippage) across all swaps + - Always use fiat mode for entering amounts + - Always preview before confirming + - Skip swaps with >5% price impact (report as finding) + - Check balance before each swap + - Track cumulative downside after each completed swap + - Stop executing swaps if cumulative downside reaches $5 + +goals: + - Test diverse swap pairs across all first-class chains + - Find broken quotes (no route, infinite loading, error messages) + - Find failed broadcasts or stuck transactions + - Find UI glitches during swap flow (overlapping text, missing data) + - Test rapid asset switching (change sell/buy mid-quote) + - Test edge cases (very small amounts, dust amounts) + - Discover which cross-chain routes work and which don't + - Check approval flows for ERC20 tokens + - Report any swap taking >120s as slow + +duration: 30min +max_findings: 30 diff --git a/e2e/fixtures/trade-exploration.yaml b/e2e/fixtures/trade-exploration.yaml new file mode 100644 index 00000000000..2e57c0f3976 --- /dev/null +++ b/e2e/fixtures/trade-exploration.yaml @@ -0,0 +1,80 @@ +name: Trade Page Exploration +description: > + Autonomous exploration of the trade page. The agent navigates freely + within the trade domain, trying edge cases, unusual inputs, and rapid + actions to discover bugs that scripted tests miss. + + The agent should snapshot anything that looks broken and report findings + as results with descriptive step names like: + "Found: infinite loading when switching assets rapidly" + +mode: exploratory +route: /trade +depends_on: + - wallet-health.yaml + +domain: + routes: + - /trade + actions: + - switch-assets + - search-assets + - change-amounts + - preview-trade + - filter-by-chain + - toggle-fiat-crypto + +steps: + - name: Set fiat mode for all exploration inputs + instruction: > + Run toggle-fiat-crypto first. If the sell input is already in fiat mode + (shows a $ placeholder), keep it unchanged. Keep fiat mode enabled for the + rest of this fixture. + expected: Fiat mode is enabled before any amount entry + screenshot: true + - name: Explore sell and buy asset switching + instruction: > + Run switch-assets and rotate through multiple sell/buy combinations on /trade. + Include at least one same-asset attempt to validate error handling. + Keep notional size <= $1 and do not confirm. + expected: Asset switching works and validation appears for invalid pairings + screenshot: true + - name: Filter by chain and search obscure assets + instruction: > + Run filter-by-chain, then search-assets in each selected chain view. + Try both common tokens and low-liquidity/obscure tokens. Capture any empty, + frozen, or misrendered search/filter states as findings. + expected: Chain filter and search update results without UI breakage + screenshot: true + - name: Change amounts across edge cases + instruction: > + Run change-amounts using $0, very small values, very large values, and rapid + amount edits while quotes are loading. Never exceed $1 per action. + expected: Amount changes are handled with stable validation and no infinite loading + screenshot: true + - name: Preview trade and stop before confirmation + instruction: > + Run preview-trade for valid quotes only. Stop at preview/confirm screen and do + not execute. Record findings for failed quotes, long quote loading (>30s), + incorrect fee/rate displays, and visible UI glitches. + expected: Preview opens when quote is valid; no trade confirmation is executed + screenshot: true + +constraints: + - Never spend more than $1 per action + - Never approve token spending above $5 + - Always use fiat mode for amounts + - Never confirm a trade (stop at preview/confirm screen) + - Maximum 15 minutes exploration time + - Stay on /trade route only + +goals: + - Find UI states that break (infinite loading, error screens, blank content) + - Try edge cases (0 amount, very large amounts, same asset both sides) + - Try rapid actions (switch assets mid-quote, spam preview button) + - Test with different chain filters and obscure tokens + - Look for visual glitches (overlapping text, broken layouts, missing icons) + - Check error handling (what happens when a quote fails?) + +duration: 15min +max_findings: 20 diff --git a/package.json b/package.json index f636c69ba37..35acbbda215 100644 --- a/package.json +++ b/package.json @@ -340,8 +340,8 @@ "@typescript-eslint/parser": "^8.16.0", "@typescript-eslint/eslint-plugin": "^8.16.0", "eslint-plugin-import": "^2.31.0", - "react-router@^6.30.0": "6.30.0", - "react-router@6.30.0": "6.30.0", + "react-router@^6.30.0": "6.30.2", + "react-router@6.30.0": "6.30.2", "@types/react": "19.1.2", "@types/react-dom": "19.1.2", "readable-stream": "3.6.0", @@ -369,14 +369,42 @@ "eslint-plugin-prettier": "5.0.0", "motion-dom": "12.7.4", "motion-utils": "12.7.2", - "porto>zod": "4.3.6" + "porto>zod": "4.3.6", + "elliptic": "6.6.1", + "@mysten/sui": "1.45.2", + "validator": "13.15.26", + "node-fetch@^2.0.0": "2.7.0", + "@ethersproject/providers>ws": "7.5.10", + "ethers>ws": "8.18.3" + }, + "peerDependencyRules": { + "allowedVersions": { + "typescript": "5.2.2", + "react": "19.2.4", + "react-dom": "19.2.4", + "@testing-library/dom": "8.20.1", + "viem": "2.43.5", + "msw": "*", + "ethers": "*", + "starknet": "9.4.0", + "cross-fetch": "4.1.0", + "zod": "*", + "bs58": "6.0.0", + "utf-8-validate": "6.0.6", + "@ton/core": "0.62.1", + "eslint": "8.57.1", + "@solana/sysvars": "*" + }, + "ignoreMissing": [ + "@solana/sysvars" + ] }, "patchedDependencies": { "@bithighlander/bitcoin-cash-js-lib@5.2.1": "patches/@bithighlander-bitcoin-cash-js-lib-npm-5.2.1-92e8f8436e.patch", "friendly-challenge@0.9.2": "patches/friendly-challenge-npm-0.9.2-db0cad46d3.patch", "react-dom@18.2.0": "patches/react-dom-npm-18.2.0-dd675bca1c.patch", "@shapeshiftoss/bitcoinjs-lib@5.2.0-shapeshift.2": "patches/@shapeshiftoss-bitcoinjs-lib-npm-5.2.0-shapeshift.2-e59ff81828.patch", - "react-router@6.30.0": "patches/react-router-npm-6.30.0-cd12804d8c.patch", + "react-router@6.30.2": "patches/react-router-npm-6.30.0-cd12804d8c.patch", "react-polyglot@0.7.2": "patches/react-polyglot-npm-0.7.2-636f85156f.patch" }, "onlyBuiltDependencies": [ diff --git a/packages/chain-adapters/src/utxo/UtxoBaseAdapter.ts b/packages/chain-adapters/src/utxo/UtxoBaseAdapter.ts index e5dd6a3dc47..b1ebd972895 100644 --- a/packages/chain-adapters/src/utxo/UtxoBaseAdapter.ts +++ b/packages/chain-adapters/src/utxo/UtxoBaseAdapter.ts @@ -377,6 +377,7 @@ export abstract class UtxoBaseAdapter implements IChainAd vout: input.vout, txid: input.txid, hex: data.hex, + ...(this.chainId === KnownChainIds.BitcoinMainnet && { sequence: 0xfffffffd }), // For Zcash, we need to pass the blockHeight and txid of each input transaction // so Ledger can add them to the PSBT and determine the correct consensus branch ID. // Only pass blockHeight if it's a valid positive number (mempool txs have blockHeight: -1) diff --git a/packages/chain-adapters/src/utxo/bitcoin/BitcoinChainAdapter.test.ts b/packages/chain-adapters/src/utxo/bitcoin/BitcoinChainAdapter.test.ts index 6372b1fb1c6..11a915fc86b 100644 --- a/packages/chain-adapters/src/utxo/bitcoin/BitcoinChainAdapter.test.ts +++ b/packages/chain-adapters/src/utxo/bitcoin/BitcoinChainAdapter.test.ts @@ -1122,6 +1122,7 @@ describe('BitcoinChainAdapter', () => { vout: 0, txid: 'adb979b44c86393236e307c45f9578d9bd064134a2779b4286c158c51ad4ab05', hex: '010000000180457afc57604fed35cc8cee29e602432c87125b9cabbcc8fc407749fe0fabfe010000006b483045022100cd627a0577d35454ced7f0a6ef8a3d3cf11c0f8696bda18062025478e0fc866002206c8ac559dc6bd851bdf00e33c1602fcaeee9d16b35d21b548529825f12dfe5ad0121027751a74f251ba2657ec2a2f374ce7d5ba1548359749823a59314c54a0670c126ffffffff02d97c0000000000001600140c0585f37ff3f9f127c9788941d6082cf7aa012173df0000000000001976a914b22138dfe140e4611b98bdb728eed04beed754c488ac00000000', + sequence: 0xfffffffd, }, ], opReturnData: undefined, @@ -1181,7 +1182,7 @@ describe('BitcoinChainAdapter', () => { }) expect(signedTx).toEqual( - '0100000000010105abd41ac558c186429b77a2344106bdd978955fc407e3363239864cb479b9ad0000000000ffffffff02900100000000000016001408450440a15ea38314c52d5c9ae6201857d7cf7a677a000000000000160014bf44db911ae5acc9cffcc1bbb9622ddda4a1112b024730440220106d6510888c70719b98069ccfa9dc92db248c1f5b7572d5cf86f3db1d371bf40220118ca57a08ed36f94772a5fbd2491a713fcb250a5ccb5e498ba70de8653763ff0121029dc27a53da073b1fea5601cf370d02d3b33cf572156c3a6df9d5c03c5dbcdcd700000000', + '0100000000010105abd41ac558c186429b77a2344106bdd978955fc407e3363239864cb479b9ad0000000000fdffffff02900100000000000016001408450440a15ea38314c52d5c9ae6201857d7cf7a677a000000000000160014bf44db911ae5acc9cffcc1bbb9622ddda4a1112b024730440220261bd026ab75ed19ee9b537204c38953593d37b1f1819fdcedfc9e494ae8503902204f38ac8cbf3145e83bc0578866fd4508fcc97bb57d70b6688253558639a8a4a50121029dc27a53da073b1fea5601cf370d02d3b33cf572156c3a6df9d5c03c5dbcdcd700000000', ) }) }) diff --git a/packages/hdwallet-core/src/wallet.ts b/packages/hdwallet-core/src/wallet.ts index b47117ade2b..12a907971e9 100644 --- a/packages/hdwallet-core/src/wallet.ts +++ b/packages/hdwallet-core/src/wallet.ts @@ -417,6 +417,10 @@ export function isVultisig(wallet: HDWallet | null): boolean { return isObject(wallet) && (wallet as any)._isVultisig } +export function isWalletConnectV2(wallet: HDWallet | null): boolean { + return isObject(wallet) && (wallet as any)._isWalletConnectV2 +} + export interface HDWalletInfo { /** * Retrieve the wallet's vendor string. diff --git a/packages/hdwallet-native/package.json b/packages/hdwallet-native/package.json index bdf45e58ddb..4468c180eb1 100644 --- a/packages/hdwallet-native/package.json +++ b/packages/hdwallet-native/package.json @@ -51,6 +51,7 @@ "bech32": "^1.1.4", "bip32": "^2.0.5", "bip39": "^3.0.2", + "bitcoinjs-message": "^2.1.0", "bs58": "^4.0.1", "bs58check": "^4.0.0", "crypto-js": "^4.2.0", diff --git a/packages/hdwallet-native/src/bitcoin.test.ts b/packages/hdwallet-native/src/bitcoin.test.ts index 8d82ff2181f..bfd6ba26081 100644 --- a/packages/hdwallet-native/src/bitcoin.test.ts +++ b/packages/hdwallet-native/src/bitcoin.test.ts @@ -504,14 +504,18 @@ describe('NativeBTCWallet', () => { await expect(wallet.btcSignTx(input as any)).rejects.toThrowError('Can not sign for this input') }) - it("doesn't support signing messages", async () => { + it('should sign messages', async () => { await expect( wallet.btcSignMessage({ coin: 'Bitcoin', addressNList: core.bip32ToAddressNList("m/44'/0'/0'/0/0"), message: 'foobar', }), - ).rejects.toThrowError('not implemented') + ).resolves.toEqual({ + address: '1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL', + signature: + '20334a3da1ff11887fc53d7e68e8b3e47c07fc0e127e78fe82b778941e88a99051691161a3b57a4e405932257bb8f75d2b5628650b3f6c755647cbb31ff89041e6', + }) }) it("doesn't support verifying messages", async () => { diff --git a/packages/hdwallet-native/src/bitcoin.ts b/packages/hdwallet-native/src/bitcoin.ts index 7f4534b29f0..baefeea374f 100644 --- a/packages/hdwallet-native/src/bitcoin.ts +++ b/packages/hdwallet-native/src/bitcoin.ts @@ -1,8 +1,10 @@ import * as bitcoin from '@shapeshiftoss/bitcoinjs-lib' import * as core from '@shapeshiftoss/hdwallet-core' import * as bchAddr from 'bchaddrjs' +import * as bitcoinMsg from 'bitcoinjs-message' import type * as Isolation from './crypto/isolation' +import { SecP256K1 } from './crypto/isolation/core' import type { NativeHDWalletBase } from './native' import * as util from './util' @@ -354,9 +356,51 @@ export function MixinNativeBTCWallet { - throw new Error('function not implemented') + async btcSignMessage(msg: core.BTCSignMessage): Promise { + const result = await this.needsMnemonic(!!this.#masterKey, async () => { + const { addressNList, coin, message } = msg + const scriptType = msg.scriptType ?? core.BTCInputScriptType.SpendAddress + + const keyPair = await util.getKeyPair(this.#masterKey!, addressNList, coin, scriptType) + + const { address } = core.createPayment(keyPair.publicKey, keyPair.network, scriptType) + if (!address) throw new Error('Could not derive address') + + const signer: bitcoinMsg.SignerAsync = { + sign: async (hash: Buffer): Promise<{ signature: Buffer; recovery: number }> => { + const recoverableSig = await SecP256K1.RecoverableSignature.signCanonically( + keyPair.node, + null, + hash, + ) + return { + signature: Buffer.from(recoverableSig.slice(0, 64)), + recovery: recoverableSig[64], + } + }, + } + + const sigOptions: bitcoinMsg.SignatureOptions | undefined = (() => { + switch (scriptType) { + case core.BTCInputScriptType.SpendWitness: + case core.BTCInputScriptType.Bech32: + return { segwitType: 'p2wpkh' as const } + case core.BTCInputScriptType.SpendP2SHWitness: + return { segwitType: 'p2sh(p2wpkh)' as const } + default: + return undefined + } + })() + + const signedMsg = await bitcoinMsg.signAsync(message, signer, true, sigOptions) + + return { + address, + signature: signedMsg.toString('hex'), + } + }) + if (!result) throw new Error('Mnemonic required') + return result } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/hdwallet-walletconnectv2/package.json b/packages/hdwallet-walletconnectv2/package.json index 08c7cd9fa18..077e2dbcf4c 100644 --- a/packages/hdwallet-walletconnectv2/package.json +++ b/packages/hdwallet-walletconnectv2/package.json @@ -28,6 +28,8 @@ "postbuild:cjs": "echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json" }, "dependencies": { + "@bitcoinerlab/secp256k1": "^1.2.0", + "@shapeshiftoss/bitcoinjs-lib": "7.0.0-shapeshift.2", "@shapeshiftoss/hdwallet-core": "workspace:^", "@walletconnect/ethereum-provider": "^2.20.2", "@walletconnect/modal": "^2.6.2", diff --git a/packages/hdwallet-walletconnectv2/src/bitcoin.ts b/packages/hdwallet-walletconnectv2/src/bitcoin.ts new file mode 100644 index 00000000000..f67433e5e3b --- /dev/null +++ b/packages/hdwallet-walletconnectv2/src/bitcoin.ts @@ -0,0 +1,253 @@ +import ecc from '@bitcoinerlab/secp256k1' +import * as bitcoin from '@shapeshiftoss/bitcoinjs-lib' +import type { + BTCAccountPath, + BTCGetAccountPaths, + BTCGetAddress, + BTCSignedMessage, + BTCSignedTx, + BTCSignMessage, + BTCSignTx, + BTCVerifyMessage, + BTCWallet, + PathDescription, +} from '@shapeshiftoss/hdwallet-core' +import { BTCInputScriptType, describeUTXOPath, slip44ByCoin } from '@shapeshiftoss/hdwallet-core' +import type EthereumProvider from '@walletconnect/ethereum-provider' + +const BIP122_BITCOIN_MAINNET_CAIP2 = 'bip122:000000000019d6689c085ae165831e93' + +function extractAddressFromCaip10(caip10Account: string): string { + const parts = caip10Account.split(':') + return parts[parts.length - 1] +} + +export function describeBTCPath( + path: number[], + coin: string, + scriptType: BTCInputScriptType, +): PathDescription { + return describeUTXOPath(path, coin, scriptType) +} + +export function btcGetAccountPaths(msg: BTCGetAccountPaths): BTCAccountPath[] { + const slip44 = slip44ByCoin(msg.coin) + if (slip44 === undefined) return [] + const bip84 = { + coin: msg.coin, + scriptType: BTCInputScriptType.SpendWitness, + addressNList: [0x80000000 + 84, 0x80000000 + slip44, 0x80000000 + msg.accountIdx], + } + + const paths: BTCAccountPath[] = [] + + if (!msg.scriptType || msg.scriptType === BTCInputScriptType.SpendWitness) { + paths.push(bip84) + } + + return paths +} + +export function btcNextAccountPath(msg: BTCAccountPath): BTCAccountPath | undefined { + if (msg.scriptType !== BTCInputScriptType.SpendWitness) return undefined + const slip44 = slip44ByCoin(msg.coin) + if (slip44 === undefined) return undefined + + const accountIdx = msg.addressNList[2] & 0x7fffffff + + return { + coin: msg.coin, + scriptType: BTCInputScriptType.SpendWitness, + addressNList: [0x80000000 + 84, 0x80000000 + slip44, 0x80000000 + accountIdx + 1], + } +} + +export async function btcGetAddress( + provider: EthereumProvider, + _msg: BTCGetAddress, +): Promise { + try { + const session = provider.session + if (!session) return null + + const bip122Accounts = session.namespaces?.bip122?.accounts + if (!bip122Accounts || bip122Accounts.length === 0) return null + + return extractAddressFromCaip10(bip122Accounts[0]) + } catch (error) { + console.error(error) + return null + } +} + +function getNetwork(coin: string): bitcoin.networks.Network { + switch (coin.toLowerCase()) { + case 'bitcoin': + return bitcoin.networks.bitcoin + default: + throw new Error(`Unsupported coin: ${coin}`) + } +} + +async function addInput(psbt: bitcoin.Psbt, input: BTCSignTx['inputs'][number]): Promise { + switch (input.scriptType) { + case BTCInputScriptType.SpendWitness: { + psbt.addInput({ + hash: input.txid, + index: input.vout, + nonWitnessUtxo: Buffer.from(input.hex, 'hex'), + ...(input.sequence !== undefined && { sequence: input.sequence }), + }) + break + } + default: + throw new Error(`Unsupported script type: ${input.scriptType}`) + } +} + +async function addOutput( + wallet: BTCWallet, + psbt: bitcoin.Psbt, + output: BTCSignTx['outputs'][number], + coin: string, +): Promise { + if (!output.amount) throw new Error('Invalid output - missing amount.') + + const address = await (async () => { + if (output.address) return output.address + + if (output.addressNList) { + const outputAddress = await wallet.btcGetAddress({ + addressNList: output.addressNList, + coin, + showDisplay: false, + }) + if (!outputAddress) throw new Error('Could not get address from wallet') + return outputAddress + } + })() + + if (!address) throw new Error('Invalid output - no address') + + psbt.addOutput({ address, value: BigInt(output.amount) }) +} + +export async function btcSignTx( + wallet: BTCWallet, + provider: EthereumProvider, + msg: BTCSignTx, +): Promise { + try { + bitcoin.initEccLib(ecc) + + const session = provider.session + if (!session) return null + + const bip122Accounts = session.namespaces?.bip122?.accounts + if (!bip122Accounts || bip122Accounts.length === 0) return null + + const address = extractAddressFromCaip10(bip122Accounts[0]) + + const network = getNetwork(msg.coin) + const psbt = new bitcoin.Psbt({ network }) + + psbt.setVersion(msg.version ?? 2) + if (msg.locktime) { + psbt.setLocktime(msg.locktime) + } + + for (const input of msg.inputs) { + await addInput(psbt, input) + } + + for (const output of msg.outputs) { + await addOutput(wallet, psbt, output, msg.coin) + } + + if (msg.opReturnData) { + const data = Buffer.from(msg.opReturnData, 'utf-8') + const embed = bitcoin.payments.embed({ data: [data] }) + const script = embed.output + if (!script) throw new Error('unable to build OP_RETURN script') + psbt.addOutput({ script, value: BigInt(0) }) + } + + const psbtBase64 = psbt.toBase64() + + const signInputs = msg.inputs.map((_input, index) => ({ + address, + index, + sighashTypes: [bitcoin.Transaction.SIGHASH_ALL], + })) + + const result = await provider.signer.request<{ psbt: string; txid?: string }>( + { + method: 'signPsbt', + params: { + account: address, + psbt: psbtBase64, + signInputs, + broadcast: false, + }, + }, + BIP122_BITCOIN_MAINNET_CAIP2, + ) + + const signedPsbt = bitcoin.Psbt.fromBase64(result.psbt, { network }) + signedPsbt.finalizeAllInputs() + const tx = signedPsbt.extractTransaction() + + const signatures = signedPsbt.data.inputs.map(input => + input.partialSig ? Buffer.from(input.partialSig[0].signature).toString('hex') : '', + ) + + return { + signatures, + serializedTx: tx.toHex(), + } + } catch (error) { + console.error(error) + return null + } +} + +export async function btcSignMessage( + provider: EthereumProvider, + msg: BTCSignMessage, +): Promise { + try { + const session = provider.session + if (!session) return null + + const bip122Accounts = session.namespaces?.bip122?.accounts + if (!bip122Accounts || bip122Accounts.length === 0) return null + + const address = extractAddressFromCaip10(bip122Accounts[0]) + + const result = await provider.signer.request<{ signature: string; address: string }>( + { + method: 'signMessage', + params: { + account: address, + message: msg.message, + }, + }, + BIP122_BITCOIN_MAINNET_CAIP2, + ) + + return { + address: result.address, + signature: result.signature, + } + } catch (error) { + console.error(error) + return null + } +} + +export async function btcVerifyMessage( + _provider: EthereumProvider, + _msg: BTCVerifyMessage, +): Promise { + return null +} diff --git a/packages/hdwallet-walletconnectv2/src/index.ts b/packages/hdwallet-walletconnectv2/src/index.ts index 366cbbcff2d..417ec2faa27 100644 --- a/packages/hdwallet-walletconnectv2/src/index.ts +++ b/packages/hdwallet-walletconnectv2/src/index.ts @@ -1,2 +1,3 @@ export * from './adapter' +export * from './bitcoin' export * from './walletconnectV2' diff --git a/packages/hdwallet-walletconnectv2/src/walletconnectV2.ts b/packages/hdwallet-walletconnectv2/src/walletconnectV2.ts index 7c3256bc311..2b0fe6cab0d 100644 --- a/packages/hdwallet-walletconnectv2/src/walletconnectV2.ts +++ b/packages/hdwallet-walletconnectv2/src/walletconnectV2.ts @@ -1,6 +1,16 @@ import type { AddEthereumChainParameter, Address, + BTCAccountPath, + BTCGetAccountPaths, + BTCGetAddress, + BTCSignedMessage, + BTCSignedTx, + BTCSignMessage, + BTCSignTx, + BTCVerifyMessage, + BTCWallet, + BTCWalletInfo, Coin, DescribePath, ETHAccountPath, @@ -14,6 +24,7 @@ import type { ETHVerifyMessage, ETHWallet, ETHWalletInfo, + GetPublicKey, HDWallet, HDWalletInfo, PathDescription, @@ -21,10 +32,19 @@ import type { Pong, PublicKey, } from '@shapeshiftoss/hdwallet-core' -import { slip44ByCoin } from '@shapeshiftoss/hdwallet-core' +import { BTCInputScriptType, slip44ByCoin } from '@shapeshiftoss/hdwallet-core' import type EthereumProvider from '@walletconnect/ethereum-provider' import isObject from 'lodash/isObject' +import { + btcGetAccountPaths, + btcGetAddress, + btcNextAccountPath, + btcSignMessage, + btcSignTx, + btcVerifyMessage, + describeBTCPath, +} from './bitcoin' import { describeETHPath, ethGetAddress, @@ -35,6 +55,12 @@ import { ethVerifyMessage, } from './ethereum' +const BIP122_OPTIONAL_NAMESPACE = { + chains: ['bip122:000000000019d6689c085ae165831e93'], + methods: ['sendTransfer', 'signPsbt', 'signMessage', 'getAccountAddresses'], + events: ['bip122_addressesChanged'], +} + export function isWalletConnectV2(wallet: HDWallet): wallet is WalletConnectV2HDWallet { return isObject(wallet) && (wallet as any)._isWalletConnectV2 } @@ -51,9 +77,9 @@ export function isWalletConnectV2(wallet: HDWallet): wallet is WalletConnectV2HD * - eth_sendRawTransaction * @see https://specs.walletconnect.com/2.0/blockchain-rpc/ethereum-rpc */ -export class WalletConnectV2WalletInfo implements HDWalletInfo, ETHWalletInfo { +export class WalletConnectV2WalletInfo implements HDWalletInfo, ETHWalletInfo, BTCWalletInfo { readonly _supportsETHInfo = true - readonly _supportsBTCInfo = false + readonly _supportsBTCInfo = true public getVendor(): string { return 'WalletConnectV2' } @@ -94,6 +120,12 @@ export class WalletConnectV2WalletInfo implements HDWalletInfo, ETHWalletInfo { switch (msg.coin) { case 'Ethereum': return describeETHPath(msg.path) + case 'Bitcoin': + return describeBTCPath( + msg.path, + msg.coin, + msg.scriptType ?? BTCInputScriptType.SpendWitness, + ) default: throw new Error('Unsupported path') } @@ -131,13 +163,40 @@ export class WalletConnectV2WalletInfo implements HDWalletInfo, ETHWalletInfo { }, ] } + + public async btcSupportsCoin(coin: Coin): Promise { + return coin === 'Bitcoin' + } + + public async btcSupportsScriptType( + coin: Coin, + scriptType?: BTCInputScriptType, + ): Promise { + if (coin !== 'Bitcoin') return false + return scriptType === undefined || scriptType === BTCInputScriptType.SpendWitness + } + + public async btcSupportsSecureTransfer(): Promise { + return false + } + + public btcSupportsNativeShapeShift(): boolean { + return false + } + + public btcGetAccountPaths(msg: BTCGetAccountPaths): BTCAccountPath[] { + return btcGetAccountPaths(msg) + } + + public btcNextAccountPath(msg: BTCAccountPath): BTCAccountPath | undefined { + return btcNextAccountPath(msg) + } } -export class WalletConnectV2HDWallet implements HDWallet, ETHWallet { +export class WalletConnectV2HDWallet implements HDWallet, ETHWallet, BTCWallet { readonly _supportsETH = true readonly _supportsETHInfo = true - readonly _supportsBTCInfo = false - readonly _supportsBTC = false + readonly _supportsBTCInfo = true readonly _isWalletConnectV2 = true readonly _supportsEthSwitchChain = true readonly _supportsAvalanche = true @@ -181,10 +240,30 @@ export class WalletConnectV2HDWallet implements HDWallet, ETHWallet { chainId: number | undefined accounts: string[] = [] ethAddress: Address | undefined + btcAddress: string | undefined + + get _supportsBTC(): boolean { + return !!this.provider.session?.namespaces?.bip122 + } constructor(provider: EthereumProvider) { this.provider = provider this.info = new WalletConnectV2WalletInfo() + this.patchSignerForNonEvmNamespaces() + } + + private patchSignerForNonEvmNamespaces(): void { + const signer = this.provider.signer + const originalConnect = signer.connect.bind(signer) + signer.connect = async (params: Parameters[0]) => { + return originalConnect({ + ...params, + optionalNamespaces: { + ...params.optionalNamespaces, + bip122: BIP122_OPTIONAL_NAMESPACE, + }, + }) + } } async getFeatures(): Promise> { @@ -299,9 +378,25 @@ export class WalletConnectV2HDWallet implements HDWallet, ETHWallet { return this.info.describePath(msg) } - public async getPublicKeys(): Promise<(PublicKey | null)[]> { - // Ethereum public keys are not exposed by the RPC API - return [] + public async getPublicKeys(msg: GetPublicKey[]): Promise<(PublicKey | null)[]> { + return await Promise.all( + msg.map(async getPublicKey => { + const { coin, scriptType } = getPublicKey + + if (coin === 'Bitcoin' && scriptType === BTCInputScriptType.SpendWitness) { + const address = await this.btcGetAddress({ + coin, + addressNList: getPublicKey.addressNList, + scriptType, + showDisplay: false, + } as BTCGetAddress) + if (!address) return null + return { xpub: address } + } + + return null + }), + ) } public async isInitialized(): Promise { @@ -403,7 +498,13 @@ export class WalletConnectV2HDWallet implements HDWallet, ETHWallet { } public async getDeviceID(): Promise { - return 'wc:' + (await this.ethGetAddress()) + const ethAddr = await this.ethGetAddress() + if (ethAddr) return 'wc:' + ethAddr + + const btcAddr = await this.btcGetAddress({ coin: 'Bitcoin' } as BTCGetAddress) + if (btcAddr) return 'wc:' + btcAddr + + return 'wc:unknown' } public async getFirmwareVersion(): Promise { @@ -427,4 +528,57 @@ export class WalletConnectV2HDWallet implements HDWallet, ETHWallet { this.chainId = parsedChainId } + + // -- BTC Methods -- + + public async btcSupportsCoin(coin: Coin): Promise { + return this.info.btcSupportsCoin(coin) + } + + public async btcSupportsScriptType( + coin: Coin, + scriptType?: BTCInputScriptType, + ): Promise { + return this.info.btcSupportsScriptType(coin, scriptType) + } + + public async btcSupportsSecureTransfer(): Promise { + return this.info.btcSupportsSecureTransfer() + } + + public btcSupportsNativeShapeShift(): boolean { + return this.info.btcSupportsNativeShapeShift() + } + + public btcGetAccountPaths(msg: BTCGetAccountPaths): BTCAccountPath[] { + return this.info.btcGetAccountPaths(msg) + } + + public btcNextAccountPath(msg: BTCAccountPath): BTCAccountPath | undefined { + return this.info.btcNextAccountPath(msg) + } + + public async btcGetAddress(msg: BTCGetAddress): Promise { + if (this.btcAddress) { + return this.btcAddress + } + const address = await btcGetAddress(this.provider, msg) + if (address) { + this.btcAddress = address + return address + } + return null + } + + public async btcSignTx(msg: BTCSignTx): Promise { + return btcSignTx(this, this.provider, msg) + } + + public async btcSignMessage(msg: BTCSignMessage): Promise { + return btcSignMessage(this.provider, msg) + } + + public async btcVerifyMessage(msg: BTCVerifyMessage): Promise { + return btcVerifyMessage(this.provider, msg) + } } diff --git a/packages/public-api/Dockerfile b/packages/public-api/Dockerfile index 74bf33079a4..b0050a9f496 100644 --- a/packages/public-api/Dockerfile +++ b/packages/public-api/Dockerfile @@ -47,8 +47,21 @@ COPY packages/types/package.json ./packages/types/ COPY packages/unchained-client/package.json ./packages/unchained-client/ COPY packages/utils/package.json ./packages/utils/ +# Copy patches directory (required by pnpm for patched dependencies in lockfile) +COPY patches/ ./patches/ + +# Use copy instead of hard-link to avoid ENOENT rename failures on Docker's overlay filesystem +ENV NPM_CONFIG_PACKAGE_IMPORT_METHOD=copy + # Install dependencies with Railway cache mount (skip build/postinstall scripts that require git/cypress/etc) -RUN corepack enable && corepack prepare pnpm@10.30.3 --activate && pnpm install --frozen-lockfile +# Retry loop: pnpm's hoisting/linking phase hits intermittent ENOENT rename failures on +# Docker overlay filesystems. Packages are cached in the store after the first attempt, +# so retries are fast. Clean node_modules between attempts to avoid partial state. +RUN corepack enable && corepack prepare pnpm@10.30.3 --activate && \ + for i in 1 2 3; do \ + pnpm install --frozen-lockfile --ignore-scripts && break || \ + { echo "pnpm install attempt $i failed, retrying..."; rm -rf node_modules; }; \ + done # Copy unchained-client config and generator files FIRST (rarely changes, enables layer caching) COPY packages/unchained-client/openapitools.json ./packages/unchained-client/ diff --git a/packages/swap-widget/Dockerfile b/packages/swap-widget/Dockerfile index 5e784f3b619..a0fec07f567 100644 --- a/packages/swap-widget/Dockerfile +++ b/packages/swap-widget/Dockerfile @@ -47,8 +47,21 @@ COPY packages/types/package.json ./packages/types/ COPY packages/unchained-client/package.json ./packages/unchained-client/ COPY packages/utils/package.json ./packages/utils/ +# Copy patches directory (required by pnpm for patched dependencies in lockfile) +COPY patches/ ./patches/ + +# Use copy instead of hard-link to avoid ENOENT rename failures on Docker's overlay filesystem +ENV NPM_CONFIG_PACKAGE_IMPORT_METHOD=copy + # Install dependencies (skip build/postinstall scripts that require git/cypress/etc) -RUN corepack enable && corepack prepare pnpm@10.30.3 --activate && pnpm install --frozen-lockfile --ignore-scripts +# Retry loop: pnpm's hoisting/linking phase hits intermittent ENOENT rename failures on +# Docker overlay filesystems. Packages are cached in the store after the first attempt, +# so retries are fast. Clean node_modules between attempts to avoid partial state. +RUN corepack enable && corepack prepare pnpm@10.30.3 --activate && \ + for i in 1 2 3; do \ + pnpm install --frozen-lockfile --ignore-scripts && break || \ + { echo "pnpm install attempt $i failed, retrying..."; rm -rf node_modules; }; \ + done # Copy unchained-client config and generator files FIRST (rarely changes, enables layer caching) COPY packages/unchained-client/openapitools.json ./packages/unchained-client/ diff --git a/packages/swap-widget/tsconfig.json b/packages/swap-widget/tsconfig.json index f50b75c5f0c..245f29c5944 100644 --- a/packages/swap-widget/tsconfig.json +++ b/packages/swap-widget/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, + "types": ["vite/client"], "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/packages/swapper/src/swappers/CetusSwapper/utils/helpers.ts b/packages/swapper/src/swappers/CetusSwapper/utils/helpers.ts index 48d521b36cd..7d308afc03a 100644 --- a/packages/swapper/src/swappers/CetusSwapper/utils/helpers.ts +++ b/packages/swapper/src/swappers/CetusSwapper/utils/helpers.ts @@ -1,6 +1,6 @@ import type { RouterDataV3 } from '@cetusprotocol/aggregator-sdk' import { AggregatorClient, Env } from '@cetusprotocol/aggregator-sdk' -import { SuiClient } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/client' +import { SuiClient } from '@mysten/sui/client' import { fromAssetId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' diff --git a/packages/utils/src/getAssetNamespaceFromChainId.ts b/packages/utils/src/getAssetNamespaceFromChainId.ts index 41230b206bc..7b41b0485bd 100644 --- a/packages/utils/src/getAssetNamespaceFromChainId.ts +++ b/packages/utils/src/getAssetNamespaceFromChainId.ts @@ -51,6 +51,8 @@ export const getAssetNamespaceFromChainId = (chainId: KnownChainIds): AssetNames return ASSET_NAMESPACE.erc20 case KnownChainIds.StarknetMainnet: return ASSET_NAMESPACE.starknetToken + case KnownChainIds.TonMainnet: + return ASSET_NAMESPACE.jetton case KnownChainIds.CosmosMainnet: case KnownChainIds.BitcoinMainnet: case KnownChainIds.BitcoinCashMainnet: @@ -59,7 +61,6 @@ export const getAssetNamespaceFromChainId = (chainId: KnownChainIds): AssetNames case KnownChainIds.ZcashMainnet: case KnownChainIds.ThorchainMainnet: case KnownChainIds.MayachainMainnet: - case KnownChainIds.TonMainnet: throw Error(`Unhandled case '${chainId}'`) default: return assertUnreachable(chainId) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61c9fb71c27..6e27b46db32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ overrides: '@typescript-eslint/parser': ^8.16.0 '@typescript-eslint/eslint-plugin': ^8.16.0 eslint-plugin-import: ^2.31.0 - react-router@^6.30.0: 6.30.0 - react-router@6.30.0: 6.30.0 + react-router@^6.30.0: 6.30.2 + react-router@6.30.0: 6.30.2 '@types/react': 19.1.2 '@types/react-dom': 19.1.2 readable-stream: 3.6.0 @@ -44,6 +44,12 @@ overrides: motion-dom: 12.7.4 motion-utils: 12.7.2 porto>zod: 4.3.6 + elliptic: 6.6.1 + '@mysten/sui': 1.45.2 + validator: 13.15.26 + node-fetch@^2.0.0: 2.7.0 + '@ethersproject/providers>ws': 7.5.10 + ethers>ws: 8.18.3 patchedDependencies: '@bithighlander/bitcoin-cash-js-lib@5.2.1': @@ -61,7 +67,7 @@ patchedDependencies: react-polyglot@0.7.2: hash: 6dc83422df610350e7e21e62ec98c1946ccf8aa92c4ac4ddc2bd97b4403ab138 path: patches/react-polyglot-npm-0.7.2-636f85156f.patch - react-router@6.30.0: + react-router@6.30.2: hash: 2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec path: patches/react-router-npm-6.30.0-cd12804d8c.patch @@ -83,7 +89,7 @@ importers: version: 1.4.5(axios@1.13.6)(typescript@5.2.2) '@cetusprotocol/cetus-sui-clmm-sdk': specifier: 5.4.0 - version: 5.4.0(@mysten/bcs@1.9.2)(@mysten/sui@1.45.0(typescript@5.2.2)) + version: 5.4.0(@mysten/bcs@1.9.2)(@mysten/sui@1.45.2(typescript@5.2.2)) '@chainflip/rpc': specifier: 2.1.1 version: 2.1.1 @@ -139,8 +145,8 @@ importers: specifier: 2.27.2 version: 2.27.2 '@mysten/sui': - specifier: 1.45.0 - version: 1.45.0(typescript@5.2.2) + specifier: 1.45.2 + version: 1.45.2(typescript@5.2.2) '@near-js/crypto': specifier: ^2.5.1 version: 2.5.1(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1)) @@ -484,8 +490,8 @@ importers: specifier: ^9.2.0 version: 9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1) react-router: - specifier: 6.30.0 - version: 6.30.0(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4) + specifier: 6.30.2 + version: 6.30.2(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4) react-router-breadcrumbs-hoc: specifier: ^4.1.0 version: 4.1.0(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) @@ -494,7 +500,7 @@ importers: version: 6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-scan: specifier: ^0.3.3 - version: 0.3.6(@types/react@19.1.2)(react-dom@19.2.4(react@19.2.4))(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@6.30.0(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4))(react@19.2.4)(rollup@4.59.0) + version: 0.3.6(@types/react@19.1.2)(react-dom@19.2.4(react@19.2.4))(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@6.30.2(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4))(react@19.2.4)(rollup@4.59.0) react-table: specifier: ^7.8.0 version: 7.8.0(react@19.2.4) @@ -812,8 +818,8 @@ importers: packages/chain-adapters: dependencies: '@mysten/sui': - specifier: 1.45.0 - version: 1.45.0(typescript@5.2.2) + specifier: 1.45.2 + version: 1.45.2(typescript@5.2.2) '@near-js/crypto': specifier: ^2.5.1 version: 2.5.1(@near-js/types@2.5.1)(@near-js/utils@2.5.1(@near-js/types@2.5.1)) @@ -1303,7 +1309,7 @@ importers: version: 4.2.0 '@ledgerhq/device-core': specifier: 0.6.9 - version: 0.6.9(react-redux@9.2.0(@types/react@19.1.2)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + version: 0.6.9(react-redux@9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@ledgerhq/hw-app-btc': specifier: 10.13.0 version: 10.13.0 @@ -1312,7 +1318,7 @@ importers: version: 6.32.9 '@ledgerhq/hw-app-eth': specifier: 7.0.0 - version: 7.0.0(react-redux@9.2.0(@types/react@19.1.2)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + version: 7.0.0(react-redux@9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@ledgerhq/hw-app-near': specifier: 6.31.10 version: 6.31.10 @@ -1385,7 +1391,7 @@ importers: version: 10.13.0 '@ledgerhq/hw-app-eth': specifier: 7.0.0 - version: 7.0.0(react-redux@9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + version: 7.0.0(react-redux@9.2.0(@types/react@19.1.2)(react@18.3.1)(redux@5.0.1))(react@18.3.1) '@ledgerhq/hw-transport': specifier: 6.31.13 version: 6.31.13 @@ -1522,6 +1528,9 @@ importers: bip39: specifier: ^3.0.2 version: 3.1.0 + bitcoinjs-message: + specifier: ^2.1.0 + version: 2.2.0 bs58: specifier: ^4.0.1 version: 4.0.1 @@ -1553,7 +1562,7 @@ importers: specifier: ^4.17.23 version: 4.17.23 node-fetch: - specifier: ^2.6.1 + specifier: 2.7.0 version: 2.7.0 p-lazy: specifier: ^3.1.0 @@ -1761,8 +1770,8 @@ importers: packages/hdwallet-seeker: dependencies: '@mysten/sui': - specifier: 1.45.0 - version: 1.45.0(typescript@5.2.2) + specifier: 1.45.2 + version: 1.45.2(typescript@5.2.2) '@shapeshiftoss/hdwallet-core': specifier: workspace:^ version: link:../hdwallet-core @@ -1858,6 +1867,12 @@ importers: packages/hdwallet-walletconnectv2: dependencies: + '@bitcoinerlab/secp256k1': + specifier: ^1.2.0 + version: 1.2.0 + '@shapeshiftoss/bitcoinjs-lib': + specifier: 7.0.0-shapeshift.2 + version: 7.0.0-shapeshift.2(typescript@5.2.2) '@shapeshiftoss/hdwallet-core': specifier: workspace:^ version: link:../hdwallet-core @@ -2050,7 +2065,7 @@ importers: specifier: 6.0.30 version: 6.0.30 '@mysten/sui': - specifier: ^1.45.2 + specifier: 1.45.2 version: 1.45.2(typescript@5.2.2) '@shapeshiftoss/bitcoinjs-lib': specifier: 7.0.0-shapeshift.0 @@ -2172,7 +2187,7 @@ importers: version: link:../caip '@shapeshiftoss/common-api': specifier: ^9.3.0 - version: 9.3.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + version: 9.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@shapeshiftoss/contracts': specifier: workspace:^ version: link:../contracts @@ -2190,16 +2205,16 @@ importers: version: 5.0.0 ethers: specifier: 6.11.1 - version: 6.11.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + version: 6.11.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) isomorphic-ws: specifier: ^4.0.1 - version: 4.0.1(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + version: 4.0.1(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) viem: specifier: 2.43.5 - version: 2.43.5(bufferutil@4.1.0)(typescript@5.2.2)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.43.5(bufferutil@4.1.0)(typescript@5.2.2)(utf-8-validate@6.0.6)(zod@3.25.76) ws: specifier: ^8.17.1 - version: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + version: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) devDependencies: '@openapitools/openapi-generator-cli': specifier: ^2.5.1 @@ -3027,7 +3042,7 @@ packages: resolution: {integrity: sha512-k3SFPEPMFLV48/xIReh8C3nbKbCsRUd8N0Nio+zEHw3eZBFSHtTFxYTiDVnn3e/f5UBB7A6MEglYIDTFMwDfYQ==} peerDependencies: '@mysten/bcs': '>=0.8.1' - '@mysten/sui': '>=1.1.2' + '@mysten/sui': 1.45.2 '@chainflip/extrinsics@1.6.2': resolution: {integrity: sha512-CseddihWRvjnuNLM94+EXqqNVexdJVEGj218tibgWqw0CnGmo9cJyHbln6sqgQPlSl4/LqBbeBR9XniJekKwZQ==} @@ -5036,10 +5051,6 @@ packages: '@mysten/ledgerjs-hw-app-sui@0.7.0': resolution: {integrity: sha512-Z2ydxg35U2tk3pThsWzPewticDnHWqAeLlh2Vp4NhYU/YFyoVxK9obh21pnvU/hmRzL+BRpYlwbX5Nuo77+lwA==} - '@mysten/sui@1.45.0': - resolution: {integrity: sha512-WYpZmrGWE/FeQaRhYrAYpHcHallj4BFIfSmjq6BkODzEhP9HfMplQAtd8AmtctwO7cpHJ+BjPUOguEAcf1gU4g==} - engines: {node: '>=18'} - '@mysten/sui@1.45.2': resolution: {integrity: sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==} engines: {node: '>=18'} @@ -5741,8 +5752,8 @@ packages: react-redux: optional: true - '@remix-run/router@1.23.0': - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + '@remix-run/router@1.23.1': + resolution: {integrity: sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==} engines: {node: '>=14.0.0'} '@remix-run/router@1.23.2': @@ -10797,12 +10808,6 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true - elliptic@6.5.4: - resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} - - elliptic@6.5.7: - resolution: {integrity: sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==} - elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -13528,19 +13533,6 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-fetch@2.6.1: - resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==} - engines: {node: 4.x || >=6.0.0} - - node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -14527,8 +14519,8 @@ packages: react: '>=16.8' react-dom: 18.2.0 - react-router@6.30.0: - resolution: {integrity: sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==} + react-router@6.30.2: + resolution: {integrity: sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' @@ -14541,7 +14533,7 @@ packages: next: '>=13.0.0' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: 18.2.0 - react-router: 6.30.0 + react-router: 6.30.2 react-router-dom: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: '@remix-run/react': @@ -16294,9 +16286,6 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@0.36.0: - resolution: {integrity: sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==} - valibot@0.38.0: resolution: {integrity: sha512-RCJa0fetnzp+h+KN9BdgYOgtsMAG9bfoJ9JSjIhFHobKWVWyzM3jjaeNTdpFK9tQtf3q1sguXeERJ/LcmdFE7w==} peerDependencies: @@ -16332,10 +16321,6 @@ packages: resolution: {integrity: sha512-pVADd6GfDT7bALYvvzhfHnfmxCDem2bG7lGY3mwHzY7ktMH/SaczoF+eKkGokQEdGKozkJvyuOiSfQEHXp4zIA==} engines: {node: '>=7.6'} - validator@13.15.20: - resolution: {integrity: sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==} - engines: {node: '>= 0.10'} - validator@13.15.26: resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} @@ -16952,18 +16937,6 @@ packages: utf-8-validate: optional: true - ws@7.4.6: - resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -17036,18 +17009,6 @@ packages: utf-8-validate: optional: true - ws@8.5.0: - resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xhr2-cookies@1.1.0: resolution: {integrity: sha512-hjXUA6q+jl/bd8ADHcVfFsSPIf+tyLIjuO9TwJC9WI6JP2zKcS7C+p56I9kCLLsaCiNT035iYvEUUzdEFj/8+g==} @@ -18278,10 +18239,10 @@ snapshots: - '@gql.tada/vue-support' - axios - '@cetusprotocol/cetus-sui-clmm-sdk@5.4.0(@mysten/bcs@1.9.2)(@mysten/sui@1.45.0(typescript@5.2.2))': + '@cetusprotocol/cetus-sui-clmm-sdk@5.4.0(@mysten/bcs@1.9.2)(@mysten/sui@1.45.2(typescript@5.2.2))': dependencies: '@mysten/bcs': 1.9.2 - '@mysten/sui': 1.45.0(typescript@5.2.2) + '@mysten/sui': 1.45.2(typescript@5.2.2) '@suchipi/femver': 1.0.0 '@syntsugar/cc-graph': 0.1.1 '@types/bn.js': 5.2.0 @@ -19716,7 +19677,7 @@ snapshots: '@ethersproject/transactions': 5.7.0 '@ethersproject/web': 5.7.1 bech32: 1.1.4 - ws: 7.4.6(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -19742,7 +19703,7 @@ snapshots: '@ethersproject/transactions': 5.8.0 '@ethersproject/web': 5.8.0 bech32: 1.1.4 - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -19785,7 +19746,7 @@ snapshots: '@ethersproject/logger': 5.7.0 '@ethersproject/properties': 5.7.0 bn.js: 5.2.3 - elliptic: 6.5.4 + elliptic: 6.6.1 hash.js: 1.1.7 '@ethersproject/signing-key@5.8.0': @@ -20477,14 +20438,14 @@ snapshots: - react - react-redux - '@ledgerhq/device-core@0.6.9(react-redux@9.2.0(@types/react@19.1.2)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + '@ledgerhq/device-core@0.6.9(react-redux@9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@ledgerhq/devices': 8.7.0 '@ledgerhq/errors': 6.29.0 '@ledgerhq/hw-transport': 6.31.13 '@ledgerhq/live-network': 2.2.3 '@ledgerhq/logs': 6.13.0 - '@ledgerhq/types-live': 6.98.0(react-redux@9.2.0(@types/react@19.1.2)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@ledgerhq/types-live': 6.98.0(react-redux@9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@noble/hashes': 1.8.0 semver: 7.7.4 transitivePeerDependencies: @@ -21643,28 +21604,6 @@ snapshots: transitivePeerDependencies: - debug - '@mysten/sui@1.45.0(typescript@5.2.2)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.0) - '@mysten/bcs': 1.9.2 - '@mysten/utils': 0.2.0 - '@noble/curves': 1.9.4 - '@noble/hashes': 1.8.0 - '@protobuf-ts/grpcweb-transport': 2.11.1 - '@protobuf-ts/runtime': 2.11.1 - '@protobuf-ts/runtime-rpc': 2.11.1 - '@scure/base': 1.2.6 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - gql.tada: 1.9.0(graphql@16.13.0)(typescript@5.2.2) - graphql: 16.13.0 - poseidon-lite: 0.2.1 - valibot: 0.36.0 - transitivePeerDependencies: - - '@gql.tada/svelte-support' - - '@gql.tada/vue-support' - - typescript - '@mysten/sui@1.45.2(typescript@5.2.2)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.0) @@ -21800,7 +21739,7 @@ snapshots: borsh: 1.0.0 http-errors: 1.7.2 optionalDependencies: - node-fetch: 2.6.7 + node-fetch: 2.7.0 transitivePeerDependencies: - encoding @@ -21813,7 +21752,7 @@ snapshots: borsh: 1.0.0 exponential-backoff: 3.1.3 optionalDependencies: - node-fetch: 2.6.7 + node-fetch: 2.7.0 transitivePeerDependencies: - encoding @@ -22541,7 +22480,7 @@ snapshots: react: 19.2.4 react-redux: 9.2.0(@types/react@19.1.2)(react@19.2.4)(redux@5.0.1) - '@remix-run/router@1.23.0': {} + '@remix-run/router@1.23.1': {} '@remix-run/router@1.23.2': {} @@ -24112,6 +24051,13 @@ snapshots: - bufferutil - utf-8-validate + '@shapeshiftoss/blockbook@9.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@shapeshiftoss/caip@8.16.8': dependencies: axios: 1.13.6(debug@4.4.3) @@ -24129,6 +24075,17 @@ snapshots: - bufferutil - utf-8-validate + '@shapeshiftoss/common-api@9.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + '@shapeshiftoss/blockbook': 9.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + bignumber.js: 9.3.1 + tsoa: 4.1.3 + uuid: 8.3.2 + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@shapeshiftoss/contracts@1.0.6(@ethersproject/address@5.8.0)(@ethersproject/contracts@5.8.0)(@ethersproject/networks@5.8.0)(@ethersproject/providers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@ethersproject/solidity@5.8.0)(bufferutil@4.1.0)(cross-fetch@4.1.0)(ipfs-only-hash@4.0.0)(multiformats@9.9.0)(typescript@5.2.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@shapeshiftoss/caip': 8.16.8 @@ -24410,6 +24367,7 @@ snapshots: - '@ethersproject/contracts' - '@ethersproject/providers' - bufferutil + - encoding - utf-8-validate '@shapeshiftoss/unchained-client@10.14.10(@ethersproject/address@5.8.0)(@ethersproject/contracts@5.8.0)(@ethersproject/networks@5.8.0)(@ethersproject/providers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@ethersproject/solidity@5.8.0)(bufferutil@4.1.0)(cross-fetch@4.1.0)(ipfs-only-hash@4.0.0)(multiformats@9.9.0)(typescript@5.2.2)(utf-8-validate@5.0.10)(zod@3.25.76)': @@ -29909,6 +29867,8 @@ snapshots: dotenv: 10.0.0 emittery: 0.8.1 type-fest: 1.2.1 + transitivePeerDependencies: + - encoding '@zag-js/dom-query@0.31.1': {} @@ -31485,7 +31445,9 @@ snapshots: cross-fetch@3.1.4: dependencies: - node-fetch: 2.6.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding cross-fetch@3.2.0: dependencies: @@ -32016,26 +31978,6 @@ snapshots: transitivePeerDependencies: - supports-color - elliptic@6.5.4: - dependencies: - bn.js: 4.12.3 - brorand: 1.1.0 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - - elliptic@6.5.7: - dependencies: - bn.js: 4.12.3 - brorand: 1.1.0 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - elliptic@6.6.1: dependencies: bn.js: 4.12.3 @@ -32943,7 +32885,7 @@ snapshots: dependencies: aes-js: 3.0.0 bn.js: 4.12.3 - elliptic: 6.5.4 + elliptic: 6.6.1 hash.js: 1.1.3 js-sha3: 0.5.7 scrypt-js: 2.0.4 @@ -32995,7 +32937,7 @@ snapshots: '@types/node': 18.15.13 aes-js: 4.0.0-beta.5 tslib: 2.4.0 - ws: 8.5.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -33008,7 +32950,7 @@ snapshots: '@types/node': 18.15.13 aes-js: 4.0.0-beta.5 tslib: 2.4.0 - ws: 8.5.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -33021,7 +32963,7 @@ snapshots: '@types/node': 22.7.5 aes-js: 4.0.0-beta.5 tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -33034,7 +32976,7 @@ snapshots: '@types/node': 22.7.5 aes-js: 4.0.0-beta.5 tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -33047,7 +32989,7 @@ snapshots: '@types/node': 22.7.5 aes-js: 4.0.0-beta.5 tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -33669,7 +33611,7 @@ snapshots: cbor: 10.0.11 cbor-bigdecimal: 10.0.11(bignumber.js@9.3.1) crc-32: 1.2.2 - elliptic: 6.5.7 + elliptic: 6.6.1 ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) hash.js: 1.1.7 js-sha3: 0.9.3 @@ -34404,6 +34346,10 @@ snapshots: dependencies: ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + isomorphic-ws@4.0.1(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + dependencies: + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + isomorphic-ws@5.0.0(ws@8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -35723,7 +35669,7 @@ snapshots: depd: 2.0.0 http-errors: 1.7.2 near-abi: 0.1.1 - node-fetch: 2.6.7 + node-fetch: 2.7.0 transitivePeerDependencies: - encoding @@ -35784,12 +35730,6 @@ snapshots: node-fetch-native@1.6.7: {} - node-fetch@2.6.1: {} - - node-fetch@2.6.7: - dependencies: - whatwg-url: 5.0.0 - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -36096,8 +36036,8 @@ snapshots: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.2.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: @@ -36968,14 +36908,14 @@ snapshots: '@remix-run/router': 1.23.2 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react-router: 6.30.0(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4) + react-router: 6.30.2(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4) - react-router@6.30.0(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4): + react-router@6.30.2(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4): dependencies: - '@remix-run/router': 1.23.0 + '@remix-run/router': 1.23.1 react: 19.2.4 - react-scan@0.3.6(@types/react@19.1.2)(react-dom@19.2.4(react@19.2.4))(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@6.30.0(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4))(react@19.2.4)(rollup@4.59.0): + react-scan@0.3.6(@types/react@19.1.2)(react-dom@19.2.4(react@19.2.4))(react-router-dom@6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-router@6.30.2(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4))(react@19.2.4)(rollup@4.59.0): dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -36997,7 +36937,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) tsx: 4.21.0 optionalDependencies: - react-router: 6.30.0(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4) + react-router: 6.30.2(patch_hash=2f48bbfed228db1edcc3e6da58f5dd6480824e45b14bcb9265c58332aa857bec)(react@19.2.4) react-router-dom: 6.30.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.1.0 transitivePeerDependencies: @@ -38391,7 +38331,7 @@ snapshots: eventemitter3: 5.0.1 google-protobuf: 3.21.4 semver: 7.7.1 - validator: 13.15.20 + validator: 13.15.26 transitivePeerDependencies: - bufferutil - debug @@ -38915,8 +38855,6 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - valibot@0.36.0: {} - valibot@0.38.0(typescript@5.2.2): optionalDependencies: typescript: 5.2.2 @@ -38942,8 +38880,6 @@ snapshots: component-type: 1.2.1 typecast: 0.0.1 - validator@13.15.20: {} - validator@13.15.26: {} valtio@1.11.2(@types/react@19.1.2)(react@19.2.4): @@ -39908,11 +39844,6 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@7.4.6(bufferutil@4.1.0)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 5.0.10 - ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 @@ -39933,11 +39864,6 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.17.1(bufferutil@4.1.0)(utf-8-validate@6.0.6): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 6.0.6 - ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 @@ -39963,16 +39889,6 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 6.0.6 - ws@8.5.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 5.0.10 - - ws@8.5.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 6.0.6 - xhr2-cookies@1.1.0: dependencies: cookiejar: 2.1.4 diff --git a/scripts/release.ts b/scripts/release.ts index 86f6f77d803..c8a3caa3d2a 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -40,21 +40,8 @@ const inquireReleaseType = async (): Promise => { return (await inquirer.prompt(questions)).releaseType } -const inquireCleanBranchOffMain = async (): Promise => { - const questions: inquirer.QuestionCollection<{ isCleanlyBranched: boolean }> = [ - { - type: 'confirm', - name: 'isCleanlyBranched', - message: 'Is your branch cleanly branched off origin/main?', - default: false, // Defaulting to false to encourage verification - }, - ] - const { isCleanlyBranched } = await inquirer.prompt(questions) - return isCleanlyBranched -} - const inquireProceedWithCommits = async (commits: string[], action: 'create' | 'merge') => { - console.log(chalk.blue(['', commits, ''].join('\n'))) + console.log(chalk.blue(['', ...commits, ''].join('\n'))) const message = action === 'create' ? 'Do you want to create a release with these commits?' @@ -366,41 +353,58 @@ const createDraftRegularPR = async (prBody: string, nextVersion: string): Promis exit(chalk.green(`Release ${nextVersion} created.`)) } -const createDraftHotfixPR = async (): Promise => { - const currentBranch = await git().revparse(['--abbrev-ref', 'HEAD']) - const { messages } = await getCommits(currentBranch as GetCommitMessagesArgs) - // TODO(0xdef1cafe): parse version bump from commit messages - const nextVersion = await getNextReleaseVersion('minor') - console.log(chalk.green('Creating draft hotfix PR...')) - await createDraftPR(`chore: hotfix release ${nextVersion}`, messages.join('\n')) - console.log(chalk.green('Draft hotfix PR created.')) - exit(chalk.green(`Hotfix release ${nextVersion} created.`)) -} - -type GetCommitMessagesArgs = 'develop' | 'release' -type GetCommitMessagesReturn = { +type GetCommitsReturn = { messages: string[] total: number } -type GetCommitMessages = (branch: GetCommitMessagesArgs) => Promise -const getCommits: GetCommitMessages = async branch => { - // Get the last release tag +const getCommits = async (branch: string): Promise => { const latestTag = await getLatestSemverTag() - - // If we have a last release tag, base the diff on that const range = latestTag ? `${latestTag}..origin/${branch}` : `origin/main..origin/${branch}` - const { all, total } = await git().log([ - '--oneline', - '--first-parent', - '--pretty=format:%s', // no hash, just conventional commit style - range, - ]) + const result = await pify(exec)(`git log --first-parent --pretty=format:"%s" ${range}`) + const stdout = typeof result === 'string' ? result : (result as { stdout: string }).stdout + const messages = stdout.trim().split('\n').filter(Boolean) - const messages = all.map(({ hash }) => hash) + const total = messages.length return { messages, total } } +type UnreleasedCommit = { hash: string; message: string } + +const getUnreleasedCommits = async (): Promise => { + const result = await pify(exec)( + 'git log --first-parent --pretty=format:"%H %s" origin/main..origin/develop', + ) + const stdout = typeof result === 'string' ? result : (result as { stdout: string }).stdout + + if (!stdout.trim()) return [] + + return stdout + .trim() + .split('\n') + .map(line => { + const spaceIdx = line.indexOf(' ') + if (spaceIdx === -1) return { hash: line, message: '' } + return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) } + }) +} + +const inquireSelectCommits = async (commits: UnreleasedCommit[]): Promise => { + const { selected } = await inquirer.prompt<{ selected: string[] }>([ + { + type: 'checkbox', + name: 'selected', + message: 'Select commits to cherry-pick into the hotfix:', + choices: commits.map(c => ({ + name: `${c.hash.slice(0, 8)} ${c.message}`, + value: c.hash, + })), + }, + ]) + + return commits.filter(c => selected.includes(c.hash)) +} + const assertCommitsToRelease = (total: number) => { if (!total) exit(chalk.red('No commits to release.')) } @@ -467,53 +471,102 @@ const doRegularRelease = async () => { } const doHotfixRelease = async () => { - const currentBranch = await git().revparse(['--abbrev-ref', 'HEAD']) - const isMain = currentBranch === 'main' + const unreleased = await getUnreleasedCommits() + if (unreleased.length === 0) { + exit(chalk.red('No unreleased commits found between origin/main and origin/develop.')) + } - if (isMain) { - console.log( - chalk.red( - 'Cannot open hotfix PRs directly off local main branch for security reasons. Please branch out to another branch first.', - ), - ) - exit() + console.log(chalk.green(`Found ${unreleased.length} unreleased commit(s).\n`)) + const selected = await inquireSelectCommits(unreleased) + if (selected.length === 0) { + exit(chalk.yellow('No commits selected. Hotfix cancelled.')) } - // Only continue if the branch is cleanly branched off origin/main since we will - // target it in the hotfix PR - const isCleanOffMain = await inquireCleanBranchOffMain() - if (!isCleanOffMain) { - exit( - chalk.yellow( - 'Please ensure your branch is cleanly branched off origin/main before proceeding.', - ), - ) + console.log(chalk.blue('\nSelected commits:')) + for (const c of selected) { + console.log(chalk.blue(` ${c.hash.slice(0, 8)} ${c.message}`)) } + console.log() - // Dev has confirmed they're clean off main, here goes nothing - await fetch() + const { shouldProceed } = await inquirer.prompt<{ shouldProceed: boolean }>([ + { + type: 'confirm', + default: true, + name: 'shouldProceed', + message: 'Proceed with cherry-picking these commits onto main?', + }, + ]) + if (!shouldProceed) exit('Hotfix cancelled.') - // Force push current branch upstream so we can getCommits from it - getCommits uses upstream for diffing - console.log(chalk.green(`Force pushing ${currentBranch} branch...`)) - await git().push(['-u', 'origin', currentBranch, '--force']) - const { messages, total } = await getCommits(currentBranch as GetCommitMessagesArgs) - assertCommitsToRelease(total) - await inquireProceedWithCommits(messages, 'create') + console.log(chalk.green('Checking out main...')) + await git().checkout(['main']) + console.log(chalk.green('Pulling main...')) + await git().pull() + const mainSha = (await git().revparse(['HEAD'])).trim() - // Merge origin/main as a paranoia check - console.log(chalk.green('Merging origin/main...')) - await git().merge(['origin/main']) + const cherryPickOrder = [...selected].reverse() + for (const c of cherryPickOrder) { + console.log(chalk.green(`Cherry-picking ${c.hash.slice(0, 8)} ${c.message}...`)) + try { + await pify(exec)(`git cherry-pick ${c.hash}`) + } catch (err) { + try { + await pify(exec)('git cherry-pick --abort') + } catch { + // no-op + } + await git().reset(['--hard', mainSha]) + const message = err instanceof Error ? err.message : String(err) + const shortHash = c.hash.slice(0, 8) + const shortMainSha = mainSha.slice(0, 8) + exit( + chalk.red( + `Cherry-pick failed for ${shortHash}: ${message}\nMain has been reset to ${shortMainSha}.`, + ), + ) + } + } - console.log(chalk.green('Setting release to current branch...')) - await git().checkout(['-B', 'release']) + const nextVersion = await getNextReleaseVersion('patch') + console.log(chalk.green(`Tagging main with version ${nextVersion}`)) + await git().tag(['-a', nextVersion, '-m', nextVersion]) + console.log(chalk.green('Pushing main with tags...')) + await git().push(['origin', 'main', '--tags']) - console.log(chalk.green('Force pushing release branch...')) - await git().push(['--force', 'origin', 'release']) + console.log(chalk.green('Resetting private to main...')) + await git().checkout(['-B', 'private']) + console.log(chalk.green('Pushing private...')) + await git().push(['--force', 'origin', 'private', '--tags']) - console.log(chalk.green('Creating draft hotfix PR...')) - await createDraftHotfixPR() + console.log(chalk.green('Checking out develop...')) + await git().checkout(['develop']) + console.log(chalk.green('Pulling develop...')) + await git().pull() + const developSha = (await git().revparse(['HEAD'])).trim() + console.log(chalk.green('Merging main back into develop...')) + try { + await git().merge(['main']) + } catch (err) { + await git() + .merge(['--abort']) + .catch(() => {}) + await git().reset(['--hard', developSha]) + const message = err instanceof Error ? err.message : String(err) + exit( + chalk.red( + `Merge into develop failed: ${message}\n` + + `Hotfix ${nextVersion} was pushed to main but develop merge failed.\n` + + `Develop has been reset to ${developSha.slice( + 0, + 8, + )}. Please merge main into develop manually.`, + ), + ) + } + console.log(chalk.green('Pushing develop...')) + await git().push(['origin', 'develop']) - exit(chalk.green('Hotfix release process completed.')) + exit(chalk.green(`Hotfix release ${nextVersion} completed successfully.`)) } type WebReleaseType = Extract @@ -547,13 +600,14 @@ const isReleaseInProgress = async (): Promise => { } const createRelease = async () => { - ;(await inquireReleaseType()) === 'Regular' ? await doRegularRelease() : doHotfixRelease() + ;(await inquireReleaseType()) === 'Regular' ? await doRegularRelease() : await doHotfixRelease() } const mergeRelease = async () => { const { messages, total } = await getCommits('release') assertCommitsToRelease(total) await inquireProceedWithCommits(messages, 'merge') + console.log(chalk.green('Checking out release...')) await git().checkout(['release']) console.log(chalk.green('Pulling release...')) @@ -581,8 +635,27 @@ const mergeRelease = async () => { await git().checkout(['develop']) console.log(chalk.green('Pulling develop...')) await git().pull() + const developSha = (await git().revparse(['HEAD'])).trim() console.log(chalk.green('Merging main back into develop...')) - await git().merge(['main']) + try { + await git().merge(['main']) + } catch (err) { + await git() + .merge(['--abort']) + .catch(() => {}) + await git().reset(['--hard', developSha]) + const message = err instanceof Error ? err.message : String(err) + exit( + chalk.red( + `Merge into develop failed: ${message}\n` + + `Release ${nextVersion} was pushed to main but develop merge failed.\n` + + `Develop has been reset to ${developSha.slice( + 0, + 8, + )}. Please merge main into develop manually.`, + ), + ) + } console.log(chalk.green('Pushing develop...')) await git().push(['origin', 'develop']) exit(chalk.green(`Release ${nextVersion} completed successfully.`)) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 2de8453d9de..d408aef0c8f 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -180,6 +180,7 @@ "expandRow": "Expand Row" }, "featureDisabled": "This feature is temporarily disabled.", + "no": "No", "yes": "Yes", "activeAccount": "Active Account", "selectAccount": "Select Account", @@ -2254,7 +2255,14 @@ "placeholder": "Gas Limit...", "tooltip": "The gas limit refers to the maximum amount of gas a user can consume to conduct a transaction." } - } + }, + "recipientAddress": "Recipient Address", + "amount": "Amount", + "amountSats": "%{amount} sats", + "psbt": "PSBT", + "broadcast": "Broadcast", + "inputs": "Inputs (%{count})", + "outputs": "Outputs (%{count})" }, "connect": { "title": "Connect your wallet to a dApp via WalletConnect and trigger transactions", @@ -2384,9 +2392,12 @@ "chainflipLending": { "headerDescription": "Supply assets to earn yield or borrow against collateral on Chainflip.", "overview": "Overview", + "myDashboard": "My Dashboard", + "allMarkets": "All Markets", + "manageLoan": "Manage Loan", "supply": { "title": "Supply", - "amount": "Supply amount", + "amount": "Amount", "available": "Available to supply", "availableTooltip": "Your free balance on Chainflip State Chain available for lending", "minimumSupply": "Minimum supply: %{amount}", @@ -2408,7 +2419,7 @@ }, "borrow": { "title": "Borrow", - "amount": "Borrow amount", + "amount": "Amount", "available": "Available to borrow", "maxLtv": "Max LTV (80%)", "currentLtv": "Current LTV", @@ -2438,6 +2449,7 @@ "totalBorrowed": "Total Borrowed", "totalBorrowedTooltip": "Total value of outstanding borrows across all Chainflip lending pools.", "pool": { + "actionPaused": "This action is temporarily paused on Chainflip", "depositFirst": "Deposit funds to Chainflip first", "noFreeBalance": "No free balance available", "noSupplyPosition": "No supply position to withdraw", @@ -2452,7 +2464,8 @@ "currentLtv": "Current LTV", "borrowCapacity": "Borrow Capacity", "borrowPowerUsed": "Borrow Power Used", - "availableToBorrow": "Available to Borrow" + "availableToBorrow": "Available to Borrow", + "actionPaused": "This action is temporarily paused on Chainflip" }, "supplyApy": "Supply APY", "supplyApyTooltip": "Annual percentage yield earned by supplying assets to this pool.", @@ -2469,7 +2482,7 @@ "title": "Collateral", "add": "Add Collateral", "remove": "Remove Collateral", - "amount": "Collateral amount", + "amount": "Amount", "availableToAdd": "Available to add", "availableToRemove": "Available to remove", "confirmAddTitle": "Confirm Add Collateral", @@ -2556,7 +2569,7 @@ "myBalancesTitle": "Chainflip Lending - My Balances", "withdraw": { "title": "Withdraw", - "amount": "Withdraw amount", + "amount": "Amount", "available": "Available to withdraw", "availableTooltip": "Your supply position in the lending pool", "confirmTitle": "Confirm Withdrawal", @@ -2603,7 +2616,7 @@ }, "repay": { "title": "Repay", - "amount": "Repay amount", + "amount": "Amount", "outstanding": "Outstanding debt", "fullRepayment": "Full repayment", "partialRepayment": "Partial repayment", @@ -2703,6 +2716,8 @@ "pools": { "pools": "Pools", "pool": "Pool", + "errorFetchingPools": "Unable to fetch pools. Please try again later.", + "noPoolsAvailable": "No pools available at this time.", "positions": "Positions", "tvl": "TVL", "volume24h": "Volume 24H", @@ -3215,6 +3230,54 @@ "complete": { "description": "Your reward of %{amountAndSymbol} is complete." } + }, + "chainflipLending": { + "deposit": { + "pending": "Your deposit of %{amountAndSymbol} to Chainflip is being processed.", + "complete": "Your deposit of %{amountAndSymbol} to Chainflip is complete.", + "failed": "Your deposit of %{amountAndSymbol} to Chainflip has failed." + }, + "supply": { + "pending": "Your supply of %{amountAndSymbol} to the lending pool is being processed.", + "complete": "Your supply of %{amountAndSymbol} to the lending pool is complete.", + "failed": "Your supply of %{amountAndSymbol} to the lending pool has failed." + }, + "withdraw": { + "pending": "Your withdrawal of %{amountAndSymbol} from the lending pool is being processed.", + "complete": "Your withdrawal of %{amountAndSymbol} from the lending pool is complete.", + "failed": "Your withdrawal of %{amountAndSymbol} from the lending pool has failed." + }, + "egress": { + "pending": "Your withdrawal of %{amountAndSymbol} from Chainflip is being processed.", + "complete": "Your withdrawal of %{amountAndSymbol} from Chainflip is complete.", + "failed": "Your withdrawal of %{amountAndSymbol} from Chainflip has failed." + }, + "addCollateral": { + "pending": "Your move of %{amountAndSymbol} from your Chainflip free balance to collateral is being processed.", + "complete": "Your move of %{amountAndSymbol} from your Chainflip free balance to collateral is complete.", + "failed": "Your move of %{amountAndSymbol} from your Chainflip free balance to collateral has failed." + }, + "removeCollateral": { + "pending": "Your move of %{amountAndSymbol} from collateral back to your Chainflip free balance is being processed.", + "complete": "Your move of %{amountAndSymbol} from collateral back to your Chainflip free balance is complete.", + "failed": "Your move of %{amountAndSymbol} from collateral back to your Chainflip free balance has failed." + }, + "borrow": { + "pending": "Your borrow of %{amountAndSymbol} to your Chainflip free balance is being processed.", + "complete": "Your borrow of %{amountAndSymbol} to your Chainflip free balance is complete.", + "failed": "Your borrow of %{amountAndSymbol} to your Chainflip free balance has failed." + }, + "repay": { + "pending": "Your repayment of %{amountAndSymbol} from your Chainflip free balance is being processed.", + "complete": "Your repayment of %{amountAndSymbol} from your Chainflip free balance is complete.", + "failed": "Your repayment of %{amountAndSymbol} from your Chainflip free balance has failed." + }, + "details": { + "operation": "Operation", + "amount": "Amount", + "transactionId": "Transaction Id", + "egressTransactionId": "Egress Transaction Id" + } } }, "yieldXYZ": { diff --git a/src/components/Layout/Header/ActionCenter/ActionCenter.tsx b/src/components/Layout/Header/ActionCenter/ActionCenter.tsx index 27b95a02ce5..b494a6d60d6 100644 --- a/src/components/Layout/Header/ActionCenter/ActionCenter.tsx +++ b/src/components/Layout/Header/ActionCenter/ActionCenter.tsx @@ -19,6 +19,7 @@ import { Virtuoso } from 'react-virtuoso' import { useActionCenterContext } from './ActionCenterContext' import { AppUpdateActionCard } from './components/AppUpdateActionCard' import { ArbitrumBridgeWithdrawActionCard } from './components/ArbitrumBridgeWithdrawActionCard' +import { ChainflipLendingActionCard } from './components/ChainflipLendingActionCard' import { EmptyState } from './components/EmptyState' import { GenericTransactionActionCard } from './components/GenericTransactionActionCard' import { LimitOrderActionCard } from './components/LimitOrderActionCard' @@ -146,6 +147,9 @@ export const ActionCenter = memo(() => { case ActionType.ArbitrumBridgeWithdraw: { return } + case ActionType.ChainflipLending: { + return + } default: return null } diff --git a/src/components/Layout/Header/ActionCenter/components/ChainflipLendingActionCard.tsx b/src/components/Layout/Header/ActionCenter/components/ChainflipLendingActionCard.tsx new file mode 100644 index 00000000000..b6222a88c46 --- /dev/null +++ b/src/components/Layout/Header/ActionCenter/components/ChainflipLendingActionCard.tsx @@ -0,0 +1,229 @@ +import { Button, ButtonGroup, HStack, Link, Stack, useDisclosure } from '@chakra-ui/react' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import { useMemo } from 'react' +import { useTranslate } from 'react-polyglot' + +import { ActionCard } from './ActionCard' +import { ActionStatusIcon } from './ActionStatusIcon' +import { ActionStatusTag } from './ActionStatusTag' + +import { Amount } from '@/components/Amount/Amount' +import { AssetIconWithBadge } from '@/components/AssetIconWithBadge' +import { RawText } from '@/components/Text' +import type { TextPropTypes } from '@/components/Text/Text' +import { Text } from '@/components/Text/Text' +import { middleEllipsis } from '@/lib/utils' +import { formatSmartDate } from '@/lib/utils/time' +import type { ChainflipLendingAction } from '@/state/slices/actionSlice/types' +import { ActionStatus, ChainflipLendingOperationType } from '@/state/slices/actionSlice/types' +import { selectAssetById } from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' + +dayjs.extend(relativeTime) + +type ChainflipLendingActionCardProps = { + action: ChainflipLendingAction +} + +export const ChainflipLendingActionCard = ({ action }: ChainflipLendingActionCardProps) => { + const translate = useTranslate() + const { chainflipLendingMetadata } = action + const operationTestIdSuffix = chainflipLendingMetadata.operationType + + const asset = useAppSelector(state => selectAssetById(state, chainflipLendingMetadata.assetId)) + + const formattedDate = useMemo(() => { + return formatSmartDate(action.updatedAt) + }, [action.updatedAt]) + + const { isOpen, onToggle } = useDisclosure({ + defaultIsOpen: action.status === ActionStatus.Pending, + }) + + const translationComponents = useMemo((): TextPropTypes['components'] | undefined => { + if (!asset) return undefined + + return { + amountAndSymbol: ( + + ), + } + }, [asset, chainflipLendingMetadata.amountCryptoPrecision]) + + const icon = useMemo(() => { + return ( + + + + ) + }, [chainflipLendingMetadata.assetId, action.status]) + + const description = useMemo(() => { + return ( + + ) + }, [ + chainflipLendingMetadata.message, + chainflipLendingMetadata.amountCryptoPrecision, + asset?.symbol, + operationTestIdSuffix, + translationComponents, + ]) + + const footer = useMemo(() => { + return + }, [action.status]) + + const operationLabel = useMemo(() => { + switch (chainflipLendingMetadata.operationType) { + case ChainflipLendingOperationType.Deposit: + return translate('chainflipLending.depositToChainflip') + case ChainflipLendingOperationType.Supply: + return translate('chainflipLending.supply.title') + case ChainflipLendingOperationType.Withdraw: + return translate('common.withdraw') + case ChainflipLendingOperationType.Egress: + return translate('chainflipLending.pool.withdrawFromChainflip') + case ChainflipLendingOperationType.AddCollateral: + return translate('chainflipLending.collateral.add') + case ChainflipLendingOperationType.RemoveCollateral: + return translate('chainflipLending.collateral.remove') + case ChainflipLendingOperationType.Borrow: + return translate('chainflipLending.borrow.title') + case ChainflipLendingOperationType.Repay: + return translate('chainflipLending.repay.title') + default: + return chainflipLendingMetadata.operationType + } + }, [chainflipLendingMetadata.operationType, translate]) + + const egressTxLink = useMemo(() => { + if (!chainflipLendingMetadata.egressTxRef || !asset?.explorerTxLink) return undefined + return `${asset.explorerTxLink}${chainflipLendingMetadata.egressTxRef}` + }, [chainflipLendingMetadata.egressTxRef, asset?.explorerTxLink]) + + const details = useMemo(() => { + return ( + + + + + {translate('actionCenter.chainflipLending.details.operation')} + + + {operationLabel} + + + + + {translate('actionCenter.chainflipLending.details.amount')} + + + + {chainflipLendingMetadata.txHash && ( + + + {translate('actionCenter.chainflipLending.details.transactionId')} + + + {middleEllipsis(chainflipLendingMetadata.txHash)} + + + )} + {chainflipLendingMetadata.egressTxRef && ( + + + {translate('actionCenter.chainflipLending.details.egressTransactionId')} + + + {middleEllipsis(chainflipLendingMetadata.egressTxRef)} + + + )} + + {egressTxLink && ( + + + + )} + + ) + }, [ + asset?.symbol, + chainflipLendingMetadata.amountCryptoPrecision, + chainflipLendingMetadata.egressTxRef, + chainflipLendingMetadata.txHash, + egressTxLink, + operationLabel, + operationTestIdSuffix, + translate, + ]) + + if (!asset) return null + + return ( + + {details} + + ) +} diff --git a/src/components/Layout/Header/ActionCenter/components/Notifications/ChainflipLendingNotification.tsx b/src/components/Layout/Header/ActionCenter/components/Notifications/ChainflipLendingNotification.tsx new file mode 100644 index 00000000000..c04a6522697 --- /dev/null +++ b/src/components/Layout/Header/ActionCenter/components/Notifications/ChainflipLendingNotification.tsx @@ -0,0 +1,94 @@ +import type { RenderProps } from '@chakra-ui/react/dist/types/toast/toast.types' +import { useMemo } from 'react' + +import { ActionIcon } from '../ActionIcon' + +import { Amount } from '@/components/Amount/Amount' +import { Text } from '@/components/Text' +import type { TextPropTypes } from '@/components/Text/Text' +import { StandardToast } from '@/components/Toast/StandardToast' +import { actionSlice } from '@/state/slices/actionSlice/actionSlice' +import type { ChainflipLendingAction } from '@/state/slices/actionSlice/types' +import { isChainflipLendingAction } from '@/state/slices/actionSlice/types' +import { selectAssetById } from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' + +type ChainflipLendingNotificationProps = { + handleClick: () => void + actionId: string +} & RenderProps + +export const ChainflipLendingNotification = ({ + handleClick, + actionId, + onClose, + status, +}: ChainflipLendingNotificationProps) => { + const actionsById = useAppSelector(actionSlice.selectors.selectActionsById) + + const action = useMemo((): ChainflipLendingAction | undefined => { + const maybeAction = actionsById[actionId] + if (!maybeAction || !isChainflipLendingAction(maybeAction)) return undefined + return maybeAction + }, [actionsById, actionId]) + + const asset = useAppSelector(state => + selectAssetById(state, action?.chainflipLendingMetadata?.assetId ?? ''), + ) + + const icon = useMemo(() => { + if (!action || !asset) return undefined + return + }, [action, asset]) + + const translationComponents = useMemo((): TextPropTypes['components'] | undefined => { + if (!action || !asset) return undefined + + return { + amountAndSymbol: ( + + ), + } + }, [action, asset]) + + const title = useMemo(() => { + if (!action || !translationComponents) return undefined + + return ( + + ) + }, [action, translationComponents, asset?.symbol]) + + if (!action || !icon || !title) return null + + const toastStatus = status === 'loading' ? 'info' : status + + return ( + + ) +} diff --git a/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseInfo.tsx b/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseInfo.tsx index c34c0723797..5a456933e2c 100644 --- a/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseInfo.tsx +++ b/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseInfo.tsx @@ -152,7 +152,7 @@ export const BackupPassphraseInfo: React.FC = props => { - + {revealed ? words : placeholders} diff --git a/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseTest.tsx b/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseTest.tsx index ebd3073b3ac..bd707838586 100644 --- a/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseTest.tsx +++ b/src/components/Layout/Header/NavBar/Native/BackupPassphraseModal/BackupPassphraseTest.tsx @@ -151,7 +151,7 @@ export const BackupPassphraseTest: React.FC = props => { translation={'modals.shapeShift.backupPassphrase.description'} mb={12} /> - + {testState.options.map((lineWords, i) => ( { - + {translate('modals.shapeShift.backupPassphrase.title')} diff --git a/src/components/MobileWalletDialog/routes/ImportWallet/ImportSeedPhrase.tsx b/src/components/MobileWalletDialog/routes/ImportWallet/ImportSeedPhrase.tsx index ecd9e820e38..8b0e94fcc54 100644 --- a/src/components/MobileWalletDialog/routes/ImportWallet/ImportSeedPhrase.tsx +++ b/src/components/MobileWalletDialog/routes/ImportWallet/ImportSeedPhrase.tsx @@ -114,7 +114,11 @@ export const ImportSeedPhrase = () => { - +