diff --git a/docs/affiliates.md b/docs/affiliates.md index bcc099c79e0..1dc4be5f9a6 100644 --- a/docs/affiliates.md +++ b/docs/affiliates.md @@ -1,95 +1,77 @@ # ShapeShift Affiliate Program -Earn revenue share on swaps executed through your integration. +Earn revenue share on swaps executed through your integration — whether you embed the +[Swap Widget](../packages/swap-widget/README.md) or call the +[Public API](../packages/public-api/docs/introduction.md) directly. + +Attribution is driven by a **partner code**: a short identifier registered to an EVM wallet address. +Pass it to the widget or the API, and ShapeShift attributes each swap to your affiliate account and +applies your configured fee automatically. ## Quick Start -### 1. Using the Swap Widget +### 1. Get a partner code + +Register at the [Affiliate Dashboard](https://dashboard.affiliate.shapeshift.com/): connect your wallet, sign +in (a wallet signature — no gas), then choose your partner code and fee. The code is mapped to your +wallet, and you'll use it everywhere below. + +### 2. Using the Swap Widget + +Pass your `partnerCode` prop. The widget renders once Reown AppKit is initialized: either pass +`walletConnectProjectId` and the widget initializes AppKit for you (shown below), or initialize AppKit +yourself in the host app and the widget reads the shared instance — see the +[widget README](../packages/swap-widget/README.md). ```tsx import { SwapWidget } from '@shapeshiftoss/swap-widget' ``` -### 2. Using the Public API - -Include these headers in your API requests: +### 3. Using the Public API -```bash -curl https://api.shapeshift.com/v1/swap/rates \ - -H "X-Affiliate-Address: 0xYourWalletAddress" \ - -H "Content-Type: application/json" \ - -d '{"sellAssetId": "...", "buyAssetId": "...", "sellAmountCryptoBaseUnit": "..."}' -``` - -Or use a partner code: +Send the `X-Partner-Code` header on the swap endpoints (`/v1/swap/rates`, `/v1/swap/quote`, +`/v1/swap/status`). The API attributes the swap to your affiliate account and applies your configured fee. ```bash -curl https://api.shapeshift.com/v1/swap/rates \ - -H "X-Partner-Code: yourcode" \ - -H "Content-Type: application/json" \ - -d '...' +curl "https://api.shapeshift.com/v1/swap/rates?sellAssetId=eip155:1/slip44:60&buyAssetId=eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&sellAmountCryptoBaseUnit=1000000000000000000" \ + -H "X-Partner-Code: your-partner-code" ``` -## Headers - -| Header | Description | -|--------|-------------| -| `X-Affiliate-Address` | Your EVM wallet address (0x...) | -| `X-Partner-Code` | Your partner code (if registered) | -| `X-Affiliate-Bps` | Override BPS (optional, 0-1000) | - -## Registering as an Affiliate +## The `X-Partner-Code` header -1. Go to the [Affiliate Dashboard](https://affiliate.shapeshift.com) -2. Connect your wallet -3. Your address is automatically registered with default BPS -4. Optionally claim a partner code +| Header | Description | +| ---------------- | ------------------------------------------------------------------------ | +| `X-Partner-Code` | Your registered partner code. Attributes the swap to your affiliate account and applies your fee. | -### Partner Codes +The partner code is the attribution mechanism: it maps to the affiliate parameters (payout address +and fee) configured for your account, so there's no separate address or bps header to send. Requests +without a partner code still succeed, but the swap is unattributed and uses the default fee. -Partner codes are short identifiers (3-32 alphanumeric characters) that map to your wallet. Benefits: +## Fees -- Easier to share than a wallet address -- Can be used in place of `X-Affiliate-Address` -- One code per affiliate +Fees are expressed in **basis points (bps)** — 1 bps = 0.01%, so 60 bps = 0.6%. The fee that applies +to swaps attributed to your partner code is configured at registration; swaps with no partner code +use ShapeShift's default fee. -Reserved codes: `shapeshift`, `ss`, `admin`, `api`, `test`, `demo` +The API surfaces the fee breakdown for each rate/quote via the `affiliateBps` (total) and +`shapeshiftBps` fields, plus `partnerBps` when the swap is attributed to a partner code — these +reflect the fee that will actually be applied, so read them per quote rather than assuming a fixed rate. -## Fee Structure +## Revenue Attribution & Reporting -| BPS | Percentage | Description | -|-----|------------|-------------| -| 10 | 0.1% | Minimum (API base) | -| 60 | 0.6% | Default | -| 100 | 1.0% | Example custom | - -- **BPS** = Basis Points (1 BPS = 0.01%) -- Fees are taken from the sell amount -- Related asset swaps (e.g., ETH → WETH) have 0% fee - -## Revenue Attribution - -Every swap executed through your integration is tracked: - -- Your affiliate address is recorded -- Volume and fees are calculated -- Stats available in the affiliate dashboard - -### Viewing Your Stats +Every swap carrying your partner code is attributed to you. Review your activity in the dashboard, or +query the API by partner code: ```bash -# Get your stats -curl "https://api.shapeshift.com/v1/affiliate/stats?address=0xYour..." +# Aggregate stats (optional startDate / endDate) +curl "https://api.shapeshift.com/v1/affiliate/stats?partnerCode=your-partner-code" -# Response +# Example response { "totalSwaps": 1234, "totalVolumeUsd": "1234567.89", @@ -97,25 +79,24 @@ curl "https://api.shapeshift.com/v1/affiliate/stats?address=0xYour..." } ``` -## API Endpoints - -### Public Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/v1/affiliate/stats?address=...` | Get swap stats | -| GET | `/v1/partner/:code` | Resolve partner code | +```bash +# Paginated swap history (optional startDate / endDate / limit / cursor) +curl "https://api.shapeshift.com/v1/affiliate/swaps?partnerCode=your-partner-code" +``` -### Authenticated Endpoints (requires wallet signature) +## Affiliate API Endpoints -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/v1/affiliate` | Register as affiliate | -| PATCH | `/v1/affiliate/:address` | Update settings | -| POST | `/v1/affiliate/claim-code` | Claim partner code | +| Method | Endpoint | Auth | Description | +| ------ | ----------------------------------------- | --------------- | ---------------------------------------- | +| GET | `/v1/affiliate/stats?partnerCode=...` | none | Aggregate swap stats for a partner code | +| GET | `/v1/affiliate/swaps?partnerCode=...` | none | Paginated swap history for a partner code | +| GET | `/v1/partner/{code}` | none | Resolve a partner code to its attribution details (address, bps split) | +| GET | `/v1/affiliate/{address}` | none | Look up an affiliate by wallet address | +| POST | `/v1/affiliate` | wallet (SIWE) | Register as an affiliate | +| PATCH | `/v1/affiliate/{address}` | wallet (SIWE) | Update affiliate settings | -## Support +Wallet-authenticated endpoints use Sign-In With Ethereum (`POST /v1/auth/siwe/nonce` → +`POST /v1/auth/siwe/verify`). Most partners never call these directly — the +[Affiliate Dashboard](https://dashboard.affiliate.shapeshift.com/) handles registration for you. -- **Dashboard**: https://affiliate.shapeshift.com -- **Discord**: https://discord.gg/shapeshift -- **Email**: affiliates@shapeshift.com +See the full request/response schemas in the [Public API reference](https://api.shapeshift.com/docs). diff --git a/docs/architecture/affiliate-data-model.md b/docs/architecture/affiliate-data-model.md deleted file mode 100644 index ab3a796e11c..00000000000 --- a/docs/architecture/affiliate-data-model.md +++ /dev/null @@ -1,310 +0,0 @@ -# Affiliate System Data Model - -## Overview - -This document defines the data model for the affiliate system, to be implemented in `shapeshift/microservices`. - -## Prisma Schema - -```prisma -// Add to prisma/schema.prisma in microservices - -model Affiliate { - id String @id @default(uuid()) - walletAddress String @unique @map("wallet_address") @db.VarChar(42) - partnerCode String? @unique @map("partner_code") @db.VarChar(32) - bps Int @default(60) - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - // Relations - swaps Swap[] @relation("AffiliateSwaps") - - @@map("affiliates") - @@index([partnerCode]) - @@index([isActive]) -} - -// Extend existing Swap model -model Swap { - // ... existing fields ... - - // New affiliate fields - affiliateAddress String? @map("affiliate_address") @db.VarChar(42) - affiliateBps Int? @map("affiliate_bps") - affiliateFeeUsd Decimal? @map("affiliate_fee_usd") @db.Decimal(18, 8) - - // Relation to affiliate (optional, for registered affiliates) - affiliate Affiliate? @relation("AffiliateSwaps", fields: [affiliateAddress], references: [walletAddress]) - - @@index([affiliateAddress]) -} - -// Partner codes can map to multiple affiliates (organizations) -model PartnerCodeMember { - id String @id @default(uuid()) - partnerCode String @map("partner_code") @db.VarChar(32) - walletAddress String @map("wallet_address") @db.VarChar(42) - role String @default("member") @db.VarChar(16) // "owner" | "admin" | "member" - createdAt DateTime @default(now()) @map("created_at") - - @@unique([partnerCode, walletAddress]) - @@map("partner_code_members") - @@index([partnerCode]) - @@index([walletAddress]) -} -``` - -## DTOs (Data Transfer Objects) - -```typescript -// packages/shared-types/src/affiliate.ts - -export interface AffiliateDto { - id: string - walletAddress: string - partnerCode: string | null - bps: number - isActive: boolean - createdAt: string - updatedAt: string -} - -export interface CreateAffiliateDto { - walletAddress: string - partnerCode?: string - bps?: number -} - -export interface UpdateAffiliateDto { - bps?: number - isActive?: boolean -} - -export interface AffiliateStatsDto { - totalSwaps: number - totalVolumeUsd: string - totalFeesEarnedUsd: string - periodStart?: string - periodEnd?: string -} - -export interface AffiliateSwapDto { - swapId: string - sellAsset: string - buyAsset: string - sellAmountUsd: string - buyAmountUsd: string - affiliateFeeUsd: string - status: string - createdAt: string - txHash?: string -} - -export interface ClaimPartnerCodeDto { - partnerCode: string -} - -export interface PartnerCodeResolutionDto { - partnerCode: string - affiliateAddress: string - bps: number -} -``` - -## API Contracts - -### GET /v1/affiliate/:address - -Get affiliate configuration by wallet address. - -**Response:** -```json -{ - "id": "uuid", - "walletAddress": "0x...", - "partnerCode": "vultisig", - "bps": 100, - "isActive": true, - "createdAt": "2026-03-12T00:00:00Z", - "updatedAt": "2026-03-12T00:00:00Z" -} -``` - -**404 Response:** Affiliate not registered (use default BPS) - -### POST /v1/affiliate - -Register as affiliate. Requires SIWE authentication. - -**Headers:** -``` -Authorization: Bearer -``` - -**Request:** -```json -{ - "walletAddress": "0x...", - "partnerCode": "mycode", - "bps": 60 -} -``` - -**Response:** Created affiliate object - -### PATCH /v1/affiliate/:address - -Update affiliate settings. Requires SIWE auth matching address. - -**Request:** -```json -{ - "bps": 100 -} -``` - -### GET /v1/affiliate/:address/stats - -Get aggregate swap statistics. - -**Query params:** -- `startDate` (optional): ISO date -- `endDate` (optional): ISO date - -**Response:** -```json -{ - "totalSwaps": 1234, - "totalVolumeUsd": "1234567.89", - "totalFeesEarnedUsd": "7407.41" -} -``` - -### GET /v1/affiliate/:address/swaps - -Get paginated swap history. - -**Query params:** -- `page` (default: 1) -- `limit` (default: 50, max: 100) -- `startDate` (optional) -- `endDate` (optional) - -**Response:** -```json -{ - "swaps": [ - { - "swapId": "uuid", - "sellAsset": "ETH", - "buyAsset": "BTC", - "sellAmountUsd": "1000.00", - "buyAmountUsd": "995.00", - "affiliateFeeUsd": "6.00", - "status": "completed", - "createdAt": "2026-03-12T00:00:00Z", - "txHash": "0x..." - } - ], - "pagination": { - "page": 1, - "limit": 50, - "total": 1234, - "pages": 25 - } -} -``` - -### GET /v1/partner/:code - -Resolve partner code to affiliate configuration. - -**Response:** -```json -{ - "partnerCode": "vultisig", - "affiliateAddress": "0x...", - "bps": 100 -} -``` - -### POST /v1/affiliate/claim-code - -Claim a partner code. Requires SIWE auth. - -**Request:** -```json -{ - "partnerCode": "mycode" -} -``` - -**Validation:** -- Code must be 3-32 alphanumeric characters -- Code must not be taken -- One code per affiliate (can transfer) - -## Migration - -```sql --- Migration: add_affiliate_tables - -CREATE TABLE affiliates ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - wallet_address VARCHAR(42) NOT NULL UNIQUE, - partner_code VARCHAR(32) UNIQUE, - bps INTEGER NOT NULL DEFAULT 60, - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() -); - -CREATE INDEX idx_affiliates_partner_code ON affiliates(partner_code); -CREATE INDEX idx_affiliates_is_active ON affiliates(is_active); - --- Add columns to existing swaps table -ALTER TABLE swaps ADD COLUMN affiliate_address VARCHAR(42); -ALTER TABLE swaps ADD COLUMN affiliate_bps INTEGER; -ALTER TABLE swaps ADD COLUMN affiliate_fee_usd DECIMAL(18, 8); - -CREATE INDEX idx_swaps_affiliate_address ON swaps(affiliate_address); - --- Partner code members (for organizations) -CREATE TABLE partner_code_members ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - partner_code VARCHAR(32) NOT NULL, - wallet_address VARCHAR(42) NOT NULL, - role VARCHAR(16) NOT NULL DEFAULT 'member', - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - UNIQUE(partner_code, wallet_address) -); - -CREATE INDEX idx_partner_code_members_code ON partner_code_members(partner_code); -CREATE INDEX idx_partner_code_members_wallet ON partner_code_members(wallet_address); -``` - -## Validation Rules - -### Wallet Address -- Must be valid EVM address (0x + 40 hex chars) -- Checksummed for storage - -### Partner Code -- 3-32 characters -- Alphanumeric + hyphens only -- Case-insensitive (stored lowercase) -- Reserved codes: `shapeshift`, `ss`, `admin`, `api`, `test` - -### BPS -- Range: 0-1000 (0% to 10%) -- Default: 60 (0.6%) -- Only modifiable by affiliate owner or admin - -## Security Considerations - -1. **SIWE Authentication**: All write operations require signed message proving wallet ownership -2. **Rate Limiting**: Stats endpoints rate-limited per address -3. **Address Validation**: Strict EVM address validation -4. **Partner Code Squatting**: Consider verification for branded codes diff --git a/docs/architecture/affiliate-system.md b/docs/architecture/affiliate-system.md index c1586ecc427..80f0c21b945 100644 --- a/docs/architecture/affiliate-system.md +++ b/docs/architecture/affiliate-system.md @@ -2,292 +2,171 @@ ## Overview -The ShapeShift affiliate system allows partners to earn revenue share on swaps executed through their integration. This document describes the current state and proposed improvements. +The ShapeShift affiliate system lets partners earn revenue share on swaps executed through their +integration (Swap Widget or Public API). Attribution is keyed on a **partner code** registered to an +EVM wallet address (the partner's attribution/settlement address). This document describes the +implemented architecture. -## Current State +> **Scope:** This repo (`shapeshift/web`) contains the widget, the Public API, and the swapper +> packages. Affiliate **persistence** (the affiliate registry, partner-code mapping, and swap +> records) lives in the separate `shapeshift/microservices` repo (the swap-service); that repo is +> authoritative for the database schema. -### Data Flow Diagram +## Data Flow ```mermaid flowchart TB - subgraph "Web App" - WA[User initiates swap] - WA --> GBps[getAffiliateBps] - GBps --> |"DEFAULT_FEE_BPS = 60"|SwapperApi - SwapperApi --> SwapperPkg[packages/swapper] + subgraph "Widget / Partner UI" + WG[SwapWidget or partner client] + WG --> |partnerCode prop / X-Partner-Code header|PublicApi end - subgraph "Widget" - WG[SwapWidget Component] - WG --> |affiliateAddress prop|ApiClient - ApiClient --> |X-Affiliate-Address header|PublicApi + subgraph "Public API (packages/public-api)" + PublicApi[swap endpoints] + PublicApi --> |resolvePartnerCode middleware|Resolve{X-Partner-Code present?} + Resolve --> |yes: resolve via swap-service|AffiliateInfo[affiliateInfo: partnerAddress, partnerBps, shapeshiftBps, affiliateBps] + Resolve --> |no: DEFAULT_AFFILIATE_BPS|AffiliateInfo + AffiliateInfo --> |total affiliateBps only|SwapperPkg[packages/swapper] end - subgraph "Public API" - PublicApi[packages/public-api] - PublicApi --> |affiliateInfo middleware|SwapperPkg + subgraph "Swapper Packages (packages/swapper)" + SwapperPkg --> |total applied as the ShapeShift affiliate fee|Swappers[Individual swappers] end - subgraph "Swapper Packages" - SwapperPkg --> |affiliateBps param|Swappers - Swappers[Individual Swappers] - Swappers --> |THORChain: ss affiliate|THORChain - Swappers --> |Jupiter: REFER4Z...|Jupiter - Swappers --> |CoW: appData|CoWProtocol - Swappers --> |etc.|OtherDEXs - end - - subgraph "Microservices" + subgraph "Microservices (shapeshift/microservices)" MS[swap-service] - MS --> |stores referralCode only|DB[(PostgreSQL)] + MS --> |partner mapping, swap records, settlement|DB[(PostgreSQL)] end - SwapperPkg --> |swap executed|MS + PublicApi --> |register swap: partnerAddress + code + bps split, for off-chain settlement|MS + PublicApi --> |resolve partner code|MS ``` -### Component Details +## Attribution model -#### 1. Web App (`src/lib/fees/`) +The key distinction: the swap charges a **single on-chain affiliate fee** — the **total** +`affiliateBps` — collected by ShapeShift's own affiliate accounts via each protocol's mechanism. The +`partnerBps` / `shapeshiftBps` breakdown and the `partnerAddress` are **attribution metadata**, not +separate on-chain fees: -**Files:** -- `src/lib/fees/constant.ts` - Defines `DEFAULT_FEE_BPS = '60'` (0.6%) -- `src/lib/fees/utils.ts` - `getAffiliateBps()` function - -**Logic:** -```typescript -// src/lib/fees/constant.ts -export const DEFAULT_FEE_BPS = '60' // basis points (0.6%) - -// src/lib/fees/utils.ts -export const getAffiliateBps = (sellAsset: Asset, buyAsset: Asset): string => { - // Related asset swaps (e.g., ETH → WETH) have 0 fee - return isRelatedAssetSwap(sellAsset, buyAsset) ? '0' : DEFAULT_FEE_BPS -} -``` +- Only the **total** `affiliateBps` is passed to the swapper (`req.affiliateInfo.affiliateBps`). The + `partnerAddress` and the bps split are **never** sent to the swapper, so the partner is **not** paid + on-chain. +- The split + partner address travel to the swap-service when the swap is registered, where they back + reporting (`/v1/affiliate/stats`, `/v1/affiliate/swaps`) and **off-chain revenue-share settlement** + to the partner. +- `affiliateBps = partnerBps + shapeshiftBps`. With no resolvable partner code the swap is + unattributed: `partnerBps` is absent and `affiliateBps = shapeshiftBps = DEFAULT_AFFILIATE_BPS`. -**Usage:** -- Called in `src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts` -- Passed to `swapperApi` endpoints in `src/state/apis/swapper/swapperApi.ts` +## Components -#### 2. Widget (`packages/swap-widget/`) +### 1. Widget (`packages/swap-widget/`) -**Props:** -```typescript -// packages/swap-widget/src/types/index.ts -export type SwapWidgetProps = { - affiliateAddress?: string // EVM address for affiliate - // ... other props -} -``` - -**API Client:** -```typescript -// packages/swap-widget/src/api/client.ts -// Passes affiliateAddress to API requests -``` +The widget takes a single `partnerCode` prop and forwards it to the Public API as the +`X-Partner-Code` header (via its internal API client). There is no affiliate-address or bps prop — +fee configuration is resolved server-side from the partner code. See the +[widget README](../../packages/swap-widget/README.md). -**Current Limitation:** Widget only passes address, cannot configure custom BPS. +### 2. Public API (`packages/public-api/`) -#### 3. Public API (`packages/public-api/`) +**Partner-code middleware** — `resolvePartnerCode` runs on the swap endpoints +(`/v1/swap/rates`, `/v1/swap/quote`, `/v1/swap/status`): -**Middleware:** ```typescript // packages/public-api/src/middleware/auth.ts -export const affiliateAddress = (req, res, next) => { - const address = req.header('X-Affiliate-Address') - if (address && EVM_ADDRESS_REGEX.test(address)) { - req.affiliateInfo = { affiliateAddress: address } +export const resolvePartnerCode = async (req, _res, next) => { + const partnerCode = req.header('X-Partner-Code') + + if (partnerCode) { + const resolved = await resolvePartnerCodeFromService(partnerCode) // -> swap-service /v1/partner/:code + if (resolved) { + req.affiliateInfo = { + partnerAddress: resolved.partnerAddress, + partnerBps: resolved.partnerBps, + shapeshiftBps: resolved.shapeshiftBps, + affiliateBps: String(Number(resolved.partnerBps) + Number(resolved.shapeshiftBps)), + partnerCode, + } + return next() + } } - next() -} -``` - -**Response Types:** -```typescript -// packages/public-api/src/types.ts -export type ApiRate = { - // ... - affiliateBps: string // Returned in responses -} -export type RatesResponse = { - rates: ApiRate[] - affiliateAddress?: string // Echo back the affiliate + // No (or unresolvable) partner code — unattributed swap uses the default fee + req.affiliateInfo = { + shapeshiftBps: env.DEFAULT_AFFILIATE_BPS, + affiliateBps: env.DEFAULT_AFFILIATE_BPS, + } + next() } ``` -**Current Limitation:** Accepts address but doesn't look up affiliate-specific BPS. - -#### 4. Swapper Packages (`packages/swapper/`) +**Bps fields** — rate, quote, and status responses carry the fee breakdown: -Each swapper handles affiliate fees differently: - -| Swapper | Affiliate Mechanism | Config Location | -|---------|---------------------|-----------------| -| THORChain | `affiliate=ss` in memo | `THORCHAIN_AFFILIATE_NAME` | -| Mayachain | `affiliate=ssmaya` in memo | `MAYACHAIN_AFFILIATE_NAME` | -| Jupiter | `REFER4Z...` contract | `JUPITER_AFFILIATE_CONTRACT_ADDRESS` | -| CoW Protocol | `appData` JSON | Included in order | -| Butter | `shapeshift` affiliate | `BUTTERSWAP_AFFILIATE` | - -**BPS Flow:** ```typescript -// packages/swapper/src/swappers/*/getTradeQuote.ts -const quote = await getQuote({ - // ... - affiliateBps, // Passed through to DEX APIs -}) +// packages/public-api/src/types.ts (BpsFields) +affiliateBps: string // total fee charged on-chain, in bps +partnerBps?: string // partner's attributed share (present when attributed) +shapeshiftBps: string // ShapeShift's attributed share ``` -#### 5. Microservices (`shapeshift/microservices`) +These reflect the [attribution model](#attribution-model) above: `affiliateBps` is the fee actually +charged; `partnerBps` / `shapeshiftBps` describe how it is attributed for settlement. -**swap-service:** -```typescript -// apps/swap-service/src/swaps/swaps.service.ts -async createSwap(data: CreateSwapDto) { - // Stores referralCode (from user-service) - // Does NOT store affiliate address or BPS - const swap = await this.prisma.swap.create({ - data: { - // ... - referralCode, // From user-service lookup - // Missing: affiliateAddress, affiliateBps - }, - }) -} -``` - -**Current Limitation:** No affiliate tracking or revenue attribution. - -#### 6. Affiliate Dashboard (`packages/affiliate-dashboard/`) - -**Current Features:** -- Address input (no auth) -- Stats display: swaps, volume, fees -- Period filtering -- Fetches from `/v1/affiliate/stats` - -**Endpoint Required:** `/v1/affiliate/stats` (needs to be implemented in microservices) - ---- - -## Gaps Identified - -### 1. No Affiliate BPS Storage -- Widget/API affiliates get whatever BPS is hardcoded -- No way to configure per-affiliate BPS -- No database table for affiliate configuration - -### 2. No Swap Attribution -- Swaps are not tagged with affiliate address -- Cannot query "swaps through affiliate X" -- Revenue attribution impossible - -### 3. No Partner Code System -- Cannot use friendly codes like "vultisig" or "venice" -- Must use raw wallet addresses - -### 4. No Authentication -- Affiliate dashboard has no wallet auth -- Anyone can view any affiliate's stats -- Cannot update own BPS without backend +**Swap registration** — when a swap is bound to a tx (first `GET /v1/swap/status` with a `txHash`), +the Public API registers it with the swap-service, forwarding the `partnerCode`, `partnerAddress`, +and the `partnerBps` / `shapeshiftBps` / `affiliateBps` breakdown so attribution can be persisted and +settled (see `routes/status/utils.ts`). -### 5. Inconsistent BPS Across Surfaces -- Web app: 60 BPS (hardcoded) -- Widget: Uses API default (should be configurable) -- Public API: No lookup, uses caller's requested BPS +### 3. Swapper Packages (`packages/swapper/`) ---- +Only the total `affiliateBps` (partner + ShapeShift) reaches the swapper. Each swapper applies it as +**ShapeShift's own** affiliate fee using that protocol's mechanism; the partner address is not +involved at this layer. -## Proposed Solution +### 4. Microservices (`shapeshift/microservices`) -See [Affiliate System Alignment Spike](../beads/web-bqz.md) for full implementation plan. +The swap-service owns affiliate persistence: the partner-code → affiliate mapping resolved by +`resolvePartnerCodeFromService`, and the swap records that back the stats/swaps endpoints. The +Public API talks to it over HTTP with an API key (`SWAP_SERVICE_API_KEY`). -### High-Level Architecture +### 5. Affiliate Dashboard (`packages/affiliate-dashboard/`) -```mermaid -flowchart TB - subgraph "Affiliate Dashboard" - AD[packages/affiliate-dashboard] - AD --> |Reown wallet auth|Arbitrum - AD --> |SIWE sign|AffiliateAPI - end +Partner-facing UI for registering an affiliate (wallet + SIWE sign-in), obtaining a partner code, and +viewing stats. It is the recommended way for partners to register rather than calling the +wallet-authenticated endpoints directly. - subgraph "Microservices" - AffiliateAPI[/v1/affiliate/*] - AffiliateAPI --> AffiliateTable[(affiliates table)] - AffiliateAPI --> SwapTable[(swaps table)] - - AffiliateTable --> |walletAddress|Lookup - AffiliateTable --> |partnerCode|Lookup - AffiliateTable --> |bps|Lookup - end +## Affiliate API Endpoints - subgraph "Public API / Widget" - PublicApi2[X-Affiliate-Address] - PublicApi2 --> |lookup BPS|AffiliateAPI - PublicApi2 --> |apply correct BPS|SwapperPkg2[Swapper] - end +Implemented in `packages/public-api` (see the [API reference](https://api.shapeshift.com/docs) for +full schemas): - subgraph "Swap Execution" - SwapperPkg2 --> |tag with affiliateAddress|SwapService - SwapService --> SwapTable - end ``` - -### Database Schema (Proposed) - -```sql --- affiliates table -CREATE TABLE affiliates ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - wallet_address VARCHAR(42) NOT NULL UNIQUE, - partner_code VARCHAR(32) UNIQUE, - bps INTEGER NOT NULL DEFAULT 60, - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- Add to swaps table -ALTER TABLE swaps ADD COLUMN affiliate_address VARCHAR(42); -ALTER TABLE swaps ADD COLUMN affiliate_bps INTEGER; +GET /v1/affiliate/stats?partnerCode=... # aggregate stats (optional startDate/endDate) +GET /v1/affiliate/swaps?partnerCode=... # paginated swap history (optional startDate/endDate/limit/cursor) +GET /v1/partner/{code} # resolve a partner code -> { partnerAddress, partnerBps, shapeshiftBps } +GET /v1/affiliate/{address} # look up an affiliate by wallet address +POST /v1/affiliate # register (SIWE-authenticated) +PATCH /v1/affiliate/{address} # update settings (SIWE-authenticated) +POST /v1/auth/siwe/nonce # SIWE: request nonce +POST /v1/auth/siwe/verify # SIWE: verify signature ``` -### API Endpoints (Proposed) - -``` -GET /v1/affiliate/:address - Get affiliate config -POST /v1/affiliate - Register (with SIWE auth) -PATCH /v1/affiliate/:address - Update BPS (with SIWE auth) -GET /v1/affiliate/:address/stats - Get swap stats -GET /v1/affiliate/:address/swaps - Get swap history -POST /v1/affiliate/claim-code - Claim partner code (with SIWE auth) -GET /v1/partner/:code - Resolve partner code to affiliate -``` +## Related Files ---- +### Public API -## Related Files +- `packages/public-api/src/middleware/auth.ts` — partner-code resolution +- `packages/public-api/src/types.ts` — `AffiliateInfo`, `BpsFields` +- `packages/public-api/src/routes/affiliate/` — stats, swaps, get/create/update affiliate +- `packages/public-api/src/routes/partner/getPartner.ts` — partner-code resolution endpoint +- `packages/public-api/src/routes/status/utils.ts` — swap registration with `partnerCode` -### Web App -- `src/lib/fees/constant.ts` -- `src/lib/fees/utils.ts` -- `src/state/apis/swapper/swapperApi.ts` -- `src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts` +### Widget & Dashboard -### Packages -- `packages/public-api/src/middleware/auth.ts` -- `packages/public-api/src/types.ts` -- `packages/swap-widget/src/types/index.ts` +- `packages/swap-widget/src/types/index.ts` — `SwapWidgetProps.partnerCode` +- `packages/swap-widget/src/api/client.ts` — sends `X-Partner-Code` - `packages/affiliate-dashboard/src/` -### Swapper Affiliate Constants -- `packages/swapper/src/swappers/ThorchainSwapper/constants.ts` -- `packages/swapper/src/swappers/MayachainSwapper/constants.ts` -- `packages/swapper/src/swappers/JupiterSwapper/utils/constants.ts` -- `packages/swapper/src/swappers/ButterSwap/utils/constants.ts` +### Microservices (separate repo: `shapeshift/microservices`) -### Microservices -- `apps/swap-service/src/swaps/swaps.service.ts` -- `apps/swap-service/src/swaps/swaps.controller.ts` +- swap-service — affiliate registry, partner-code mapping, swap records diff --git a/packages/affiliate-dashboard/src/App.tsx b/packages/affiliate-dashboard/src/App.tsx index bf8d1a15da1..b197d615171 100644 --- a/packages/affiliate-dashboard/src/App.tsx +++ b/packages/affiliate-dashboard/src/App.tsx @@ -36,6 +36,7 @@ export const App = (): React.JSX.Element => { const affiliateAddress = isConnected && address ? address : '' const configQuery = useAffiliateConfig(affiliateAddress) + const partnerCode = configQuery.data?.partnerCode ?? '' const periods = useMemo( () => generatePeriods(configQuery.data?.createdAt), @@ -44,8 +45,8 @@ export const App = (): React.JSX.Element => { const currentPeriod = periods.find(p => p.key === selectedKey) ?? periods[0] - const statsQuery = useAffiliateStats(affiliateAddress, currentPeriod) - const swapsQuery = useAffiliateSwaps(affiliateAddress, currentPeriod) + const statsQuery = useAffiliateStats(partnerCode, currentPeriod) + const swapsQuery = useAffiliateSwaps(partnerCode, currentPeriod) const actions = useAffiliateActions({ affiliateAddress, authHeaders }) const swaps = useMemo( diff --git a/packages/affiliate-dashboard/src/hooks/useAffiliateConfig.ts b/packages/affiliate-dashboard/src/hooks/useAffiliateConfig.ts index a6fdcc345d0..77695cffee7 100644 --- a/packages/affiliate-dashboard/src/hooks/useAffiliateConfig.ts +++ b/packages/affiliate-dashboard/src/hooks/useAffiliateConfig.ts @@ -9,7 +9,7 @@ const AffiliateConfigSchema = z.object({ id: z.string(), walletAddress: z.string(), receiveAddress: z.string().nullable(), - partnerCode: z.string().nullable(), + partnerCode: z.string(), partnerBps: z.number(), shapeshiftBps: z.number(), isActive: z.boolean(), diff --git a/packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts b/packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts index 405d822e555..a6068263270 100644 --- a/packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts +++ b/packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts @@ -20,8 +20,8 @@ const ApiResponseSchema = z.object({ totalFeesEarnedUsd: NumericString, }) -const fetchStats = async (address: string, period: Period): Promise => { - const params = new URLSearchParams({ address }) +const fetchStats = async (partnerCode: string, period: Period): Promise => { + const params = new URLSearchParams({ partnerCode }) if (period.startDate) params.append('startDate', period.startDate) if (period.endDate) params.append('endDate', period.endDate) @@ -37,12 +37,12 @@ const fetchStats = async (address: string, period: Period): Promise => useQuery({ - queryKey: ['affiliate', 'stats', address, period.startDate, period.endDate], - queryFn: () => fetchStats(address, period), - enabled: Boolean(address), + queryKey: ['affiliate', 'stats', partnerCode, period.startDate, period.endDate], + queryFn: () => fetchStats(partnerCode, period), + enabled: Boolean(partnerCode), placeholderData: keepPreviousData, }) diff --git a/packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts b/packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts index 36fb8c65fb7..f7f14591669 100644 --- a/packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts +++ b/packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts @@ -51,12 +51,12 @@ export interface AffiliateSwapsPage { } const fetchSwaps = async ( - address: string, + partnerCode: string, period: Period, cursor: string | undefined, ): Promise => { const params = new URLSearchParams({ - address, + partnerCode, limit: String(SWAPS_PER_PAGE), }) @@ -69,13 +69,13 @@ const fetchSwaps = async ( } export const useAffiliateSwaps = ( - address: string, + partnerCode: string, period: Period, ): UseInfiniteQueryResult, Error> => useInfiniteQuery({ - queryKey: ['affiliate', 'swaps', address, period.key], - queryFn: ({ pageParam }) => fetchSwaps(address, period, pageParam), - enabled: Boolean(address), + queryKey: ['affiliate', 'swaps', partnerCode, period.key], + queryFn: ({ pageParam }) => fetchSwaps(partnerCode, period, pageParam), + enabled: Boolean(partnerCode), initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => { const next = lastPage.nextCursor?.trim() diff --git a/packages/caip/package.json b/packages/caip/package.json index 3b76dbd75ca..93bb60fa25c 100644 --- a/packages/caip/package.json +++ b/packages/caip/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/caip", - "version": "8.16.7", + "version": "8.16.9", "description": "CAIP Implementation", "repository": "https://github.com/shapeshift/web", "license": "MIT", diff --git a/packages/chain-adapters/package.json b/packages/chain-adapters/package.json index 951a74b67d9..f8af39f8374 100644 --- a/packages/chain-adapters/package.json +++ b/packages/chain-adapters/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/chain-adapters", - "version": "11.3.9", + "version": "11.4.0", "repository": "https://github.com/shapeshift/web", "license": "MIT", "type": "module", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 5454620ec88..e27a7d5148e 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/contracts", - "version": "1.0.6", + "version": "1.0.7", "repository": "https://github.com/shapeshift/web", "license": "MIT", "type": "module", diff --git a/packages/contracts/src/abis/foxy.ts b/packages/contracts/src/abis/foxy.ts deleted file mode 100644 index 448865b4145..00000000000 --- a/packages/contracts/src/abis/foxy.ts +++ /dev/null @@ -1,675 +0,0 @@ -export const FOXY_ABI = [ - { - inputs: [], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'uint256', - name: 'epoch', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'rebase', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'index', - type: 'uint256', - }, - ], - name: 'LogRebase', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'uint256', - name: 'epoch', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'timestamp', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'totalSupply', - type: 'uint256', - }, - ], - name: 'LogSupply', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipPulled', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipPushed', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'from', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'to', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [], - name: 'DOMAIN_SEPARATOR', - outputs: [ - { - internalType: 'bytes32', - name: '', - type: 'bytes32', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_owner', - type: 'address', - }, - { - internalType: 'address', - name: '_spender', - type: 'address', - }, - ], - name: 'allowance', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_spender', - type: 'address', - }, - { - internalType: 'uint256', - name: '_value', - type: 'uint256', - }, - ], - name: 'approve', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_gons', - type: 'uint256', - }, - ], - name: 'balanceForGons', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_wallet', - type: 'address', - }, - ], - name: 'balanceOf', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'circulatingSupply', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'decimals', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_spender', - type: 'address', - }, - { - internalType: 'uint256', - name: '_subtractedValue', - type: 'uint256', - }, - ], - name: 'decreaseAllowance', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'getIndex', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getOwner', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - ], - name: 'gonsForBalance', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_spender', - type: 'address', - }, - { - internalType: 'uint256', - name: '_addedValue', - type: 'uint256', - }, - ], - name: 'increaseAllowance', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'index', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_stakingContract', - type: 'address', - }, - ], - name: 'initialize', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'initializer', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - ], - name: 'nonces', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'deadline', - type: 'uint256', - }, - { - internalType: 'uint8', - name: 'v', - type: 'uint8', - }, - { - internalType: 'bytes32', - name: 'r', - type: 'bytes32', - }, - { - internalType: 'bytes32', - name: 's', - type: 'bytes32', - }, - ], - name: 'permit', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'pullOwner', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_newOwner', - type: 'address', - }, - ], - name: 'pushOwner', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_profit', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_epoch', - type: 'uint256', - }, - ], - name: 'rebase', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - name: 'rebases', - outputs: [ - { - internalType: 'uint256', - name: 'epoch', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'rebase', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'totalStakedBefore', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'totalStakedAfter', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'amountRebased', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'index', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'blockNumberOccurred', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'renounceOwner', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'stakingContract', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_to', - type: 'address', - }, - { - internalType: 'uint256', - name: '_value', - type: 'uint256', - }, - ], - name: 'transfer', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_from', - type: 'address', - }, - { - internalType: 'address', - name: '_to', - type: 'address', - }, - { - internalType: 'uint256', - name: '_value', - type: 'uint256', - }, - ], - name: 'transferFrom', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/packages/contracts/src/abis/index.ts b/packages/contracts/src/abis/index.ts index ced06a76a04..4b2adff5122 100644 --- a/packages/contracts/src/abis/index.ts +++ b/packages/contracts/src/abis/index.ts @@ -4,17 +4,12 @@ export * from './arbRetryableTx' export * from './arbSys' export * from './farming' export * from './evergreenFarming' -export * from './foxy' export * from './foxyStaking' export * from './l1ArbitrumGateway' export * from './l1OrbitCustomGateway' export * from './l2ArbitrumGateway' -export * from './liquidityReserve' export * from './rfox' export * from './thorchainRouter' -export * from './tokeManager' -export * from './tokePool' -export * from './tokeRewardHash' export * from './iUniswapV2Pair' export * from './uniswapV2Router02' export * from './iUniswapV3Pool' diff --git a/packages/contracts/src/abis/liquidityReserve.ts b/packages/contracts/src/abis/liquidityReserve.ts deleted file mode 100644 index 05b2307bf2e..00000000000 --- a/packages/contracts/src/abis/liquidityReserve.ts +++ /dev/null @@ -1,547 +0,0 @@ -export const LIQUIDITY_RESERVE_ABI = [ - { - inputs: [ - { - internalType: 'address', - name: '_stakingToken', - type: 'address', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'FeeChanged', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipPulled', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipPushed', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'from', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'to', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [], - name: 'BASIS_POINTS', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'MINIMUM_LIQUIDITY', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - ], - name: 'addLiquidity', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - ], - name: 'allowance', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'approve', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'account', - type: 'address', - }, - ], - name: 'balanceOf', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'decimals', - outputs: [ - { - internalType: 'uint8', - name: '', - type: 'uint8', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'subtractedValue', - type: 'uint256', - }, - ], - name: 'decreaseAllowance', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'fee', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getOwner', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - internalType: 'uint256', - name: 'addedValue', - type: 'uint256', - }, - ], - name: 'increaseAllowance', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_stakingContract', - type: 'address', - }, - { - internalType: 'address', - name: '_rewardToken', - type: 'address', - }, - ], - name: 'initialize', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'initializer', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - { - internalType: 'address', - name: '_recipient', - type: 'address', - }, - ], - name: 'instantUnstake', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'pullOwner', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '_newOwner', - type: 'address', - }, - ], - name: 'pushOwner', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_amount', - type: 'uint256', - }, - ], - name: 'removeLiquidity', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'renounceOwner', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'rewardToken', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_fee', - type: 'uint256', - }, - ], - name: 'setFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'stakingContract', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'stakingToken', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'recipient', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'transfer', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'sender', - type: 'address', - }, - { - internalType: 'address', - name: 'recipient', - type: 'address', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'transferFrom', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'unstakeAllRewardTokens', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/packages/contracts/src/abis/tokeManager.ts b/packages/contracts/src/abis/tokeManager.ts deleted file mode 100644 index 81a0be56f22..00000000000 --- a/packages/contracts/src/abis/tokeManager.ts +++ /dev/null @@ -1,508 +0,0 @@ -export const TOKE_MANAGER_ABI = [ - { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'bytes32', name: 'id', type: 'bytes32' }, - { indexed: false, internalType: 'address', name: 'controller', type: 'address' }, - ], - name: 'ControllerRegistered', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'bytes32', name: 'id', type: 'bytes32' }, - { indexed: false, internalType: 'address', name: 'controller', type: 'address' }, - ], - name: 'ControllerUnregistered', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'uint256', name: 'duration', type: 'uint256' }], - name: 'CycleDurationSet', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'uint256', name: 'timestamp', type: 'uint256' }], - name: 'CycleRolloverComplete', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'uint256', name: 'timestamp', type: 'uint256' }], - name: 'CycleRolloverStarted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'bytes32', name: 'controller', type: 'bytes32' }, - { indexed: false, internalType: 'address', name: 'adapaterAddress', type: 'address' }, - { indexed: false, internalType: 'bytes', name: 'data', type: 'bytes' }, - ], - name: 'DeploymentStepExecuted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address', name: 'fxStateSender', type: 'address' }, - { indexed: false, internalType: 'address', name: 'destinationOnL2', type: 'address' }, - ], - name: 'DestinationsSet', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'bool', name: 'eventSendSet', type: 'bool' }], - name: 'EventSendSet', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address', name: 'pool', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'LiquidityMovedToManager', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address', name: 'pool', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'LiquidityMovedToPool', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address[]', name: 'addresses', type: 'address[]' }, - { indexed: false, internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' }, - ], - name: 'ManagerSwept', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'uint256', name: 'nextCycleStartTime', type: 'uint256' }, - ], - name: 'NextCycleStartSet', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'address', name: 'pool', type: 'address' }], - name: 'PoolRegistered', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'address', name: 'pool', type: 'address' }], - name: 'PoolUnregistered', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { indexed: true, internalType: 'bytes32', name: 'previousAdminRole', type: 'bytes32' }, - { indexed: true, internalType: 'bytes32', name: 'newAdminRole', type: 'bytes32' }, - ], - name: 'RoleAdminChanged', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { indexed: true, internalType: 'address', name: 'account', type: 'address' }, - { indexed: true, internalType: 'address', name: 'sender', type: 'address' }, - ], - name: 'RoleGranted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { indexed: true, internalType: 'address', name: 'account', type: 'address' }, - { indexed: true, internalType: 'address', name: 'sender', type: 'address' }, - ], - name: 'RoleRevoked', - type: 'event', - }, - { - inputs: [], - name: 'ADMIN_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'DEFAULT_ADMIN_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'MID_CYCLE_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'ROLLOVER_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'START_ROLLOVER_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: '_eventSend', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'string', name: 'rewardsIpfsHash', type: 'string' }], - name: 'completeRollover', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'currentCycle', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'currentCycleIndex', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'cycleDuration', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - name: 'cycleRewardsHashes', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'destinations', - outputs: [ - { internalType: 'contract IFxStateSender', name: 'fxStateSender', type: 'address' }, - { internalType: 'address', name: 'destinationOnL2', type: 'address' }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - components: [ - { - components: [ - { internalType: 'bytes32', name: 'controllerId', type: 'bytes32' }, - { internalType: 'bytes', name: 'data', type: 'bytes' }, - ], - internalType: 'struct IManager.ControllerTransferData[]', - name: 'cycleSteps', - type: 'tuple[]', - }, - ], - internalType: 'struct IManager.MaintenanceExecution', - name: 'params', - type: 'tuple', - }, - ], - name: 'executeMaintenance', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - components: [ - { - components: [ - { internalType: 'address', name: 'pool', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - internalType: 'struct IManager.PoolTransferData[]', - name: 'poolData', - type: 'tuple[]', - }, - { - components: [ - { internalType: 'bytes32', name: 'controllerId', type: 'bytes32' }, - { internalType: 'bytes', name: 'data', type: 'bytes' }, - ], - internalType: 'struct IManager.ControllerTransferData[]', - name: 'cycleSteps', - type: 'tuple[]', - }, - { internalType: 'address[]', name: 'poolsForWithdraw', type: 'address[]' }, - { internalType: 'bool', name: 'complete', type: 'bool' }, - { internalType: 'string', name: 'rewardsIpfsHash', type: 'string' }, - ], - internalType: 'struct IManager.RolloverExecution', - name: 'params', - type: 'tuple', - }, - ], - name: 'executeRollover', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'getControllers', - outputs: [{ internalType: 'bytes32[]', name: '', type: 'bytes32[]' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getCurrentCycle', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getCurrentCycleIndex', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getCycleDuration', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getPools', - outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], - name: 'getRoleAdmin', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'uint256', name: 'index', type: 'uint256' }, - ], - name: 'getRoleMember', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], - name: 'getRoleMemberCount', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getRolloverStatus', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'grantRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'hasRole', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'uint256', name: '_cycleDuration', type: 'uint256' }, - { internalType: 'uint256', name: '_nextCycleStartTime', type: 'uint256' }, - ], - name: 'initialize', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'nextCycleStartTime', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'id', type: 'bytes32' }, - { internalType: 'address', name: 'controller', type: 'address' }, - ], - name: 'registerController', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'pool', type: 'address' }], - name: 'registerPool', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - name: 'registeredControllers', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'renounceRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes32', name: 'role', type: 'bytes32' }, - { internalType: 'address', name: 'account', type: 'address' }, - ], - name: 'revokeRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'rolloverStarted', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: 'duration', type: 'uint256' }], - name: 'setCycleDuration', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: '_fxStateSender', type: 'address' }, - { internalType: 'address', name: '_destinationOnL2', type: 'address' }, - ], - name: 'setDestinations', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bool', name: '_eventSendSet', type: 'bool' }], - name: 'setEventSend', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: '_nextCycleStartTime', type: 'uint256' }], - name: 'setNextCycleStartTime', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], - name: 'setupRole', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'startCycleRollover', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address[]', name: 'poolAddresses', type: 'address[]' }], - name: 'sweep', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes32', name: 'id', type: 'bytes32' }], - name: 'unRegisterController', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'pool', type: 'address' }], - name: 'unRegisterPool', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/packages/contracts/src/abis/tokePool.ts b/packages/contracts/src/abis/tokePool.ts deleted file mode 100644 index cf539b11f24..00000000000 --- a/packages/contracts/src/abis/tokePool.ts +++ /dev/null @@ -1,308 +0,0 @@ -export const TOKE_POOL_ABI = [ - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'owner', type: 'address' }, - { indexed: true, internalType: 'address', name: 'spender', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address', name: 'fxStateSender', type: 'address' }, - { indexed: false, internalType: 'address', name: 'destinationOnL2', type: 'address' }, - ], - name: 'DestinationsSet', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'bool', name: 'eventSendSet', type: 'bool' }], - name: 'EventSendSet', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, - { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, - ], - name: 'OwnershipTransferred', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }], - name: 'Paused', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'from', type: 'address' }, - { indexed: true, internalType: 'address', name: 'to', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'value', type: 'uint256' }, - ], - name: 'Transfer', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }], - name: 'Unpaused', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'address', name: 'requestor', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'WithdrawalRequested', - type: 'event', - }, - { - inputs: [], - name: '_eventSend', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], - name: 'approveManager', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'decimals', - outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'subtractedValue', type: 'uint256' }, - ], - name: 'decreaseAllowance', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], - name: 'deposit', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'account', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'depositFor', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'destinations', - outputs: [ - { internalType: 'contract IFxStateSender', name: 'fxStateSender', type: 'address' }, - { internalType: 'address', name: 'destinationOnL2', type: 'address' }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'addedValue', type: 'uint256' }, - ], - name: 'increaseAllowance', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'contract ERC20Upgradeable', name: '_underlyer', type: 'address' }, - { internalType: 'contract IManager', name: '_manager', type: 'address' }, - { internalType: 'string', name: '_name', type: 'string' }, - { internalType: 'string', name: '_symbol', type: 'string' }, - ], - name: 'initialize', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'manager', - outputs: [{ internalType: 'contract IManager', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'owner', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, - { - inputs: [], - name: 'paused', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'renounceOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], - name: 'requestWithdrawal', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: '', type: 'address' }], - name: 'requestedWithdrawals', - outputs: [ - { internalType: 'uint256', name: 'minCycle', type: 'uint256' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: '_fxStateSender', type: 'address' }, - { internalType: 'address', name: '_destinationOnL2', type: 'address' }, - ], - name: 'setDestinations', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bool', name: '_eventSendSet', type: 'bool' }], - name: 'setEventSend', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'recipient', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'sender', type: 'address' }, - { internalType: 'address', name: 'recipient', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], - name: 'transferOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'underlyer', - outputs: [{ internalType: 'contract ERC20Upgradeable', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' }, - { - inputs: [{ internalType: 'uint256', name: 'requestedAmount', type: 'uint256' }], - name: 'withdraw', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'withheldLiquidity', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/packages/contracts/src/abis/tokeRewardHash.ts b/packages/contracts/src/abis/tokeRewardHash.ts deleted file mode 100644 index 1a431ee0aa6..00000000000 --- a/packages/contracts/src/abis/tokeRewardHash.ts +++ /dev/null @@ -1,71 +0,0 @@ -export const TOKE_REWARD_HASH_ABI = [ - { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'uint256', name: 'cycleIndex', type: 'uint256' }, - { indexed: false, internalType: 'string', name: 'latestClaimableHash', type: 'string' }, - { indexed: false, internalType: 'string', name: 'cycleHash', type: 'string' }, - ], - name: 'CycleHashAdded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, - { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, - ], - name: 'OwnershipTransferred', - type: 'event', - }, - { - inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - name: 'cycleHashes', - outputs: [ - { internalType: 'string', name: 'latestClaimable', type: 'string' }, - { internalType: 'string', name: 'cycle', type: 'string' }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'latestCycleIndex', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'owner', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'renounceOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'uint256', name: 'index', type: 'uint256' }, - { internalType: 'string', name: 'latestClaimableIpfsHash', type: 'string' }, - { internalType: 'string', name: 'cycleIpfsHash', type: 'string' }, - ], - name: 'setCycleHashes', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], - name: 'transferOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/packages/public-api/docs/introduction.md b/packages/public-api/docs/introduction.md index 0b038a18743..048f7d2daf6 100644 --- a/packages/public-api/docs/introduction.md +++ b/packages/public-api/docs/introduction.md @@ -1,17 +1,49 @@ -The ShapeShift Public API enables developers to integrate multi-chain swap functionality into their applications. Access rates from multiple DEX aggregators and execute swaps across supported blockchains. +The ShapeShift Public API lets you integrate multi-chain swap functionality into your application. Fetch rates from multiple DEX aggregators and bridges, build executable quotes, and track swaps across supported blockchains. -There are two ways to integrate: +## Base URL -1. **Swap Widget SDK** — Drop-in React component with built-in UI, wallet connection, and multi-chain support. Fastest way to integrate. -2. **REST API** — Build your own swap UI using the endpoints below. Full control over UX. +``` +https://api.shapeshift.com +``` -## Affiliate Tracking (Optional) +All endpoints are versioned under `/v1` (e.g. `https://api.shapeshift.com/v1/swap/rates`). This interactive reference is served at `https://api.shapeshift.com/docs`, and the raw OpenAPI document at `https://api.shapeshift.com/docs/json`. -Include a `X-Partner-Code` header with your registered partner code (e.g. `vultisig`, `venice`) to attribute swaps for affiliate fee tracking. The API resolves the code to the registered affiliate address and BPS automatically. Register a partner code at the affiliate dashboard. This is optional — all endpoints work without it. +## Two ways to integrate -## Asset IDs +1. **Swap Widget SDK** — a drop-in React component with built-in UI, wallet connection, and multi-chain support. The fastest path. See the **Swap Widget SDK** section. +2. **REST API** — call the endpoints directly and build your own UI for full control over UX. See the **REST API Guide** section, then the per-endpoint reference below. + +## Affiliate tracking (optional) + +Send an `X-Partner-Code` header with your registered partner code (e.g. `your-partner-code`) on the swap endpoints to attribute swaps for affiliate revenue share. The API attributes the swap to your affiliate account and applies your configured fee (bps) automatically. All endpoints work without it — unattributed swaps use the default fee. See the [Affiliate Program guide](https://github.com/shapeshift/web/blob/develop/docs/affiliates.md) for how to obtain a code. + +## Asset IDs (CAIP-19) + +Assets are identified with [CAIP-19](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-19.md): `{chainId}/{assetNamespace}:{assetReference}` -Assets use CAIP-19 format: `{chainId}/{assetNamespace}:{assetReference}` - Native ETH: `eip155:1/slip44:60` - USDC on Ethereum: `eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` - Native BTC: `bip122:000000000019d6689c085ae165831e93/slip44:0` + +Chains use [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) (e.g. `eip155:1`). Use `GET /v1/chains` and `GET /v1/assets` to discover supported values. + +## Errors + +Errors return the appropriate HTTP status and a JSON body: + +```json +{ "error": "Human-readable message", "code": "MACHINE_CODE", "details": [] } +``` + +`code` and `details` are present where applicable (e.g. `QUOTE_NOT_FOUND`, `TX_HASH_REQUIRED`, `TX_HASH_MISMATCH`, `RATE_LIMIT_EXCEEDED`, validation `details`). + +## Rate limiting + +Endpoints are rate limited per IP on a fixed 60-second window. A global limit applies across all endpoints, and individual endpoint groups (data, rates, quote, status, affiliate) have their own independent limits on top of it — so a request counts against both. When either is exceeded, the API returns `429` with code `RATE_LIMIT_EXCEEDED` and these headers: + +- `Retry-After` — seconds until the window resets +- `RateLimit-Limit` — max requests allowed per window +- `RateLimit-Remaining` — requests remaining in the current window +- `RateLimit-Reset` — seconds until the window resets + +Back off using `Retry-After` and avoid polling faster than necessary (see the REST API Guide for polling guidance). diff --git a/packages/public-api/docs/rest-api-guide.md b/packages/public-api/docs/rest-api-guide.md index 23a1f9a5b1e..1092ac48403 100644 --- a/packages/public-api/docs/rest-api-guide.md +++ b/packages/public-api/docs/rest-api-guide.md @@ -1,42 +1,80 @@ -Step-by-step guide for integrating swaps via the REST API. +A step-by-step guide to executing a swap via the REST API. Full request/response schemas for every endpoint are in the reference sections below — this guide covers the flow and the semantics that aren't obvious from the schemas alone (quote expiry, status polling, errors). + +All paths are relative to `https://api.shapeshift.com`. Send `X-Partner-Code: ` on the swap endpoints to attribute swaps for affiliate revenue (optional). + +## 1. Discover chains and assets -## 1. Get Supported Chains ``` GET /v1/chains +GET /v1/assets?chainId=eip155:1&limit=100&offset=0 ``` -## 2. Get Supported Assets -``` -GET /v1/assets -``` +`GET /v1/assets` supports optional `chainId`, `limit` (1–1000, default 100), and `offset` (default 0) query params for filtering and pagination. Use `GET /v1/assets/count` to size pagination. Look up a single asset with `GET /v1/assets/{assetId}` (the asset ID is a full CAIP-19 string). + +## 2. Get rates -## 3. Get Swap Rates ``` GET /v1/swap/rates?sellAssetId=eip155:1/slip44:60&buyAssetId=eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&sellAmountCryptoBaseUnit=1000000000000000000 -X-Partner-Code: your-partner-code (optional) +X-Partner-Code: your-partner-code ``` -## 4. Get Executable Quote +Optional `slippageTolerancePercentageDecimal` (e.g. `0.01` for 1%). The response returns a `rates` array (one entry per swapper, each with its own `swapperName`, amounts, fees, and an optional per-swapper `error`) plus `timestamp` and `expiresAt`. **Rates are indicative**, expire quickly (`expiresAt` ≈ 30s after issue), and are for display/comparison — request a quote to execute. + +## 3. Get an executable quote + ``` POST /v1/swap/quote -X-Partner-Code: your-partner-code (optional) +Content-Type: application/json +X-Partner-Code: your-partner-code { "sellAssetId": "eip155:1/slip44:60", "buyAssetId": "bip122:000000000019d6689c085ae165831e93/slip44:0", "sellAmountCryptoBaseUnit": "1000000000000000000", "swapperName": "Relay", - "receiveAddress": "bc1q...", - "sendAddress": "0x..." + "receiveAddress": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + "sendAddress": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "slippageTolerancePercentageDecimal": "0.01", + "accountNumber": 0 } ``` -## 5. Execute the Swap -Use the returned `transactionData` to build and sign a transaction with the user's wallet, then broadcast it to the network. +- `swapperName` comes from the rate you chose in step 2. +- `slippageTolerancePercentageDecimal` is optional; `accountNumber` is optional (defaults to `0`) and is needed for chains that derive addresses per account index (e.g. UTXO/Cosmos). +- The response includes a `quoteId` (needed for status tracking), an `approval` object (whether an ERC-20 approval is required and the approval tx to send first), and a `steps` array. Each step may include `transactionData` — a discriminated union on `type` (`evm`, `solana`, `utxo_psbt`, `utxo_deposit`, `cosmos`) — describing exactly what to sign for that chain. +- Quotes expire: honor the `expiresAt` timestamp (≈ 60s after issue). Request a fresh quote rather than submitting an expired one. + +## 4. Execute the swap + +The API does **not** broadcast transactions — your application signs and broadcasts with the user's wallet: + +1. If `approval.isRequired` is true and `approval.approvalTx` is present, send the approval transaction first and wait for it to confirm. +2. For each step with `transactionData`, build, sign, and broadcast the transaction according to its `type` (EVM tx, Solana instructions, UTXO PSBT/deposit, or Cosmos message). +3. Capture the resulting transaction hash for status tracking. + +## 5. Track status -## 6. Check Swap Status ``` GET /v1/swap/status?quoteId=&txHash=0x... ``` -On the **first call**, include `txHash` to bind the transaction to the quote and start tracking. Subsequent polls can omit `txHash`. +- On the **first call after broadcasting**, include `txHash` to bind it to the quote and begin tracking. This sets status to `submitted`. Subsequent polls can omit `txHash`. +- `status` is one of `submitted`, `confirmed`, `failed`. Poll until `confirmed` or `failed`; a `buyTxHash` appears once the destination transaction is known. +- Poll at a modest interval (e.g. every 5–15s) and respect rate-limit headers. Stop polling on a terminal status. + +### Status errors + +- `404` `QUOTE_NOT_FOUND` — the quote is unknown or has expired from the store. Request a new quote. +- `400` `TX_HASH_REQUIRED` — no `txHash` was provided and none is bound yet; pass the broadcast tx hash. +- `409` `TX_HASH_MISMATCH` — a different `txHash` is already bound to this quote. + +## Affiliate reporting (optional) + +Once live, partners can review attributed activity by partner code: + +``` +GET /v1/affiliate/stats?partnerCode=your-partner-code +GET /v1/affiliate/swaps?partnerCode=your-partner-code +``` + +You can also resolve a code to its attribution details (partner address and bps split) with `GET /v1/partner/{code}`. diff --git a/packages/public-api/docs/swap-widget-sdk.md b/packages/public-api/docs/swap-widget-sdk.md index d8a244da3c3..54f64802f7b 100644 --- a/packages/public-api/docs/swap-widget-sdk.md +++ b/packages/public-api/docs/swap-widget-sdk.md @@ -1,20 +1,26 @@ -The `@shapeshiftoss/swap-widget` package is a drop-in React component that provides a complete swap interface. It handles asset selection, rate comparison, wallet connection, transaction signing, and status tracking. +The `@shapeshiftoss/swap-widget` package is a drop-in React component that provides a complete swap interface — asset selection, rate comparison, wallet connection, transaction signing, and status tracking — backed by this API. + +> 📖 **The canonical, always-current reference is the package README:** +> [`packages/swap-widget/README.md`](https://github.com/shapeshift/web/blob/develop/packages/swap-widget/README.md). +> It documents every prop, the theming API, supported chains/swappers, and exported hooks. This page is a short orientation; defer to the README for details. ## Installation ```bash npm install @shapeshiftoss/swap-widget -# or -yarn add @shapeshiftoss/swap-widget ``` -**Peer dependencies** (install alongside the widget): +Install the peer dependencies alongside it (React, wagmi/viem, React Query, and Reown AppKit): ```bash -npm install react react-dom +npm install react react-dom wagmi @wagmi/core viem \ + @tanstack/react-query \ + @reown/appkit @reown/appkit-adapter-wagmi \ + @reown/appkit-adapter-bitcoin @reown/appkit-adapter-solana \ + @solana/wallet-adapter-wallets @solana/web3.js ``` -**CSS** — You must import the widget stylesheet: +Import the stylesheet once (required for the widget to render correctly): ```tsx import '@shapeshiftoss/swap-widget/style.css' @@ -23,351 +29,27 @@ import '@shapeshiftoss/swap-widget/style.css' ## Quick Start ```tsx -import { SwapWidget } from '@shapeshiftoss/swap-widget' import '@shapeshiftoss/swap-widget/style.css' -function App() { - return ( - console.log('Success:', txHash)} - /> - ) -} -``` - ---- - -## Wallet Connection Modes - -The widget supports two wallet connection strategies. Choose the one that matches your application. - -### Mode 1: External Wallet (Recommended for dApps) - -**Use this if your application already has a wallet connection** (wagmi, ethers, viem, RainbowKit, ConnectKit, AppKit, etc.). Pass the connected wallet to the widget — no duplicate wallet modals. - -```tsx import { SwapWidget } from '@shapeshiftoss/swap-widget' -import '@shapeshiftoss/swap-widget/style.css' -import { useWalletClient } from 'wagmi' - -function SwapPage() { - const { data: walletClient } = useWalletClient() - - return ( - { - // Trigger YOUR app's wallet connection modal - openYourConnectModal() - }} - theme="dark" - /> - ) -} -``` - -| Prop | Purpose | -|------|---------| -| `walletClient` | A viem `WalletClient` from your existing wallet setup | -| `onConnectWallet` | Called when the user clicks "Connect" inside the widget — open your own modal | -| `enableWalletConnection` | Leave as `false` (default) — the widget won't render its own connect UI | - -This mode creates its own read-only wagmi config internally for balance fetching. It does **not** interfere with your application's wagmi provider or AppKit instance. - -### Mode 2: Built-in Wallet Connection (Standalone) - -**Use this if your page has no wallet infrastructure.** The widget manages wallet connections internally via Reown AppKit, supporting EVM, Bitcoin, and Solana wallets. - -```tsx -import { SwapWidget } from '@shapeshiftoss/swap-widget' -import '@shapeshiftoss/swap-widget/style.css' function App() { return ( console.log('Success:', txHash)} /> ) } ``` -Get a WalletConnect project ID at [cloud.walletconnect.com](https://cloud.walletconnect.com). - -When `enableWalletConnection` is true, the widget: -- Shows a "Connect" button that opens a multi-chain wallet modal -- Supports MetaMask, WalletConnect, Coinbase Wallet, and other EVM wallets -- Supports Bitcoin wallets via WalletConnect -- Supports Phantom, Solflare, and other Solana wallets - -> **Important: AppKit Singleton Constraint** -> -> The built-in wallet connection uses Reown AppKit, which is a **global singleton** — only one AppKit instance can exist per page. If your page already uses AppKit or Web3Modal, the widget's modal will conflict with yours. -> -> **If your dApp already has AppKit/Web3Modal**: Use **Mode 1 (External Wallet)** instead. Pass your connected `walletClient` to the widget and handle wallet connection yourself. -> -> **If your page has no wallet setup**: Mode 2 works perfectly — the widget is the only AppKit instance on the page. - ---- - -## Props Reference - -### Core Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `partnerCode` | `string` | — | Your registered partner code for affiliate fee attribution. Register at the affiliate dashboard. | -| `apiBaseUrl` | `string` | — | Custom API base URL | -| `theme` | `ThemeMode \| ThemeConfig` | `"dark"` | Theme mode or full theme configuration | -| `showPoweredBy` | `boolean` | `true` | Show "Powered by ShapeShift" branding | -| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance percentage | -| `ratesRefetchInterval` | `number` | `15000` | Rate refresh interval in milliseconds | - -### Wallet Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `walletClient` | `WalletClient` | — | Viem wallet client for EVM transactions (Mode 1) | -| `enableWalletConnection` | `boolean` | `false` | Enable built-in wallet modal (Mode 2) | -| `walletConnectProjectId` | `string` | — | Required for Mode 2 | -| `onConnectWallet` | `() => void` | — | Callback when user clicks "Connect" (Mode 1) | -| `defaultReceiveAddress` | `string` | — | Lock the receive address to a specific value | - -### Asset Filtering Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `defaultSellAsset` | `Asset` | ETH | Initial sell asset | -| `defaultBuyAsset` | `Asset` | USDC | Initial buy asset | -| `allowedChainIds` | `ChainId[]` | all | Restrict both sides to these chains | -| `disabledChainIds` | `ChainId[]` | `[]` | Hide chains from both selectors | -| `disabledAssetIds` | `AssetId[]` | `[]` | Hide assets from both selectors | -| `sellAllowedChainIds` | `ChainId[]` | — | Restrict sell side to these chains | -| `buyAllowedChainIds` | `ChainId[]` | — | Restrict buy side to these chains | -| `sellAllowedAssetIds` | `AssetId[]` | — | Restrict sell side to these assets | -| `buyAllowedAssetIds` | `AssetId[]` | — | Restrict buy side to these assets | -| `sellDisabledChainIds` | `ChainId[]` | `[]` | Hide chains from sell selector | -| `buyDisabledChainIds` | `ChainId[]` | `[]` | Hide chains from buy selector | -| `sellDisabledAssetIds` | `AssetId[]` | `[]` | Hide assets from sell selector | -| `buyDisabledAssetIds` | `AssetId[]` | `[]` | Hide assets from buy selector | -| `allowedSwapperNames` | `SwapperName[]` | all | Restrict to specific swappers | -| `isBuyAssetLocked` | `boolean` | `false` | Prevent changing the buy asset | - -### Callback Props - -| Prop | Type | Description | -|------|-------|-------------| -| `onSwapSuccess` | `(txHash: string) => void` | Called when a swap succeeds | -| `onSwapError` | `(error: Error) => void` | Called when a swap fails | -| `onAssetSelect` | `(type: 'sell' \| 'buy', asset: Asset) => void` | Called when user selects an asset | - ---- - -## Theming - -### Simple Mode - -```tsx - - -``` - -### Custom Theme - -```tsx -const theme: ThemeConfig = { - mode: 'dark', - accentColor: '#3861fb', - backgroundColor: '#0a0a14', - cardColor: '#12121c', - textColor: '#ffffff', - borderRadius: '12px', - fontFamily: 'Inter, sans-serif', - borderColor: '#2a2a3e', - secondaryTextColor: '#a0a0b0', - mutedTextColor: '#6b6b80', - inputColor: '#1a1a2e', - hoverColor: '#1e1e32', - buttonVariant: 'filled', // 'filled' or 'outline' -} - - -``` - -| Property | Type | Description | -|----------|------|-------------| -| `mode` | `'light' \| 'dark'` | Base theme mode (required) | -| `accentColor` | `string` | Buttons, focus states, active elements | -| `backgroundColor` | `string` | Widget background | -| `cardColor` | `string` | Card and panel backgrounds | -| `textColor` | `string` | Primary text | -| `borderRadius` | `string` | Border radius (e.g. `'12px'`) | -| `fontFamily` | `string` | Font family | -| `borderColor` | `string` | Border colors | -| `secondaryTextColor` | `string` | Secondary labels | -| `mutedTextColor` | `string` | Muted/disabled text | -| `inputColor` | `string` | Input field background | -| `hoverColor` | `string` | Hover state background | -| `buttonVariant` | `'filled' \| 'outline'` | Button style | - ---- - -## Integration Examples - -### Restrict to Ethereum + Polygon Only - -```tsx -import { SwapWidget, EVM_CHAIN_IDS } from '@shapeshiftoss/swap-widget' - - -``` - -### Lock Buy Asset (Payment Widget) - -```tsx -const usdcAsset = { - assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - chainId: 'eip155:1', - symbol: 'USDC', - name: 'USD Coin', - precision: 6, -} - - -``` - -### Use Specific Swappers Only - -```tsx -import { SwapWidget, SwapperName } from '@shapeshiftoss/swap-widget' - - -``` +## Key things to know ---- +- **Wallet connection is built in.** The widget connects wallets via Reown AppKit (EVM, Bitcoin, and Solana). +- **AppKit must be initialized before the widget mounts.** Either pass `walletConnectProjectId` and the widget initializes AppKit for you (get a free project ID at [dashboard.reown.com](https://dashboard.reown.com)), or call `createAppKit()` yourself in the host app (with a wagmi EVM adapter) before mounting the widget — the widget detects the shared singleton and supplies its own providers, so you wrap it in none. Pair the host-owned path with `showConnectButton={false}` to drive connection from your own UI. This requires `@reown/appkit*`/`wagmi`/`viem` to dedupe to a single shared copy. See the [README](https://github.com/shapeshift/web/blob/develop/packages/swap-widget/README.md#wallet-connection). +- **`partnerCode` drives affiliate attribution.** It is forwarded to this API as the `X-Partner-Code` header. See the [Affiliate Program guide](https://github.com/shapeshift/web/blob/develop/docs/affiliates.md). +- **Chain/asset filtering** uses the `sellFilters` and `buyFilters` props (objects with `allowedChainIds` / `disabledChainIds` / `allowedAssetIds` / `disabledAssetIds`). See the README for the full prop list and examples. -## Exported Hooks - -These hooks can be used outside the widget to build custom UI with ShapeShift asset data. - -```tsx -import { - useAssets, - useAssetById, - useChains, - useAssetsByChainId, - useAssetSearch, -} from '@shapeshiftoss/swap-widget' -``` - -| Hook | Return Type | Description | -|------|-------------|-------------| -| `useAssets()` | `{ data: Asset[], isLoading, ... }` | All available assets | -| `useAssetById(assetId)` | `{ data: Asset \| undefined, ... }` | Single asset by CAIP-19 ID | -| `useChains()` | `{ data: ChainInfo[], ... }` | All chains with native assets | -| `useAssetsByChainId(chainId)` | `{ data: Asset[], ... }` | All assets on a chain | -| `useAssetSearch(query, chainId?)` | `{ data: Asset[], ... }` | Search by symbol or name | - -All hooks return React Query result objects with `data`, `isLoading`, `error`, `refetch`, etc. - -## Exported Utilities - -```tsx -import { - formatAmount, - parseAmount, - truncateAddress, - isEvmChainId, - getEvmNetworkId, - getChainType, - getChainName, - getChainIcon, - getChainColor, - getBaseAsset, - getExplorerTxLink, - EVM_CHAIN_IDS, - UTXO_CHAIN_IDS, - COSMOS_CHAIN_IDS, - OTHER_CHAIN_IDS, -} from '@shapeshiftoss/swap-widget' -``` - ---- - -## Supported Chains - -The widget natively supports all EVM chains, Bitcoin, and Solana. Other chains (Cosmos, Starknet, NEAR, TON, Tron, Sui, etc.) are available via redirect to [app.shapeshift.com](https://app.shapeshift.com). - -| Chain | Chain ID | Type | -|-------|----------|------| -| Ethereum | `eip155:1` | EVM | -| Arbitrum One | `eip155:42161` | EVM | -| Avalanche C-Chain | `eip155:43114` | EVM | -| Base | `eip155:8453` | EVM | -| Berachain | `eip155:80094` | EVM | -| Blast | `eip155:81457` | EVM | -| BNB Smart Chain | `eip155:56` | EVM | -| BOB | `eip155:60808` | EVM | -| Cronos | `eip155:25` | EVM | -| Flow EVM | `eip155:747` | EVM | -| Gnosis | `eip155:100` | EVM | -| Hemi | `eip155:43111` | EVM | -| HyperEVM | `eip155:999` | EVM | -| Ink | `eip155:57073` | EVM | -| Katana | `eip155:747474` | EVM | -| Linea | `eip155:59144` | EVM | -| Mantle | `eip155:5000` | EVM | -| MegaETH | `eip155:4326` | EVM | -| Mode | `eip155:34443` | EVM | -| Monad | `eip155:143` | EVM | -| Optimism | `eip155:10` | EVM | -| Plasma | `eip155:9745` | EVM | -| Plume | `eip155:98866` | EVM | -| Polygon | `eip155:137` | EVM | -| Scroll | `eip155:534352` | EVM | -| Soneium | `eip155:1868` | EVM | -| Sonic | `eip155:146` | EVM | -| Story | `eip155:1514` | EVM | -| Unichain | `eip155:130` | EVM | -| World Chain | `eip155:480` | EVM | -| zkSync Era | `eip155:324` | EVM | -| Bitcoin | `bip122:000000000019d6689c085ae165831e93` | UTXO | -| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Solana | - -## Supported Swappers (15) - -THORChain, MAYAChain, CoW Swap, 0x, Portals, Chainflip, Jupiter, Relay, ButterSwap, Bebop, Arbitrum Bridge, NEAR Intents, Cetus, Sun.io, AVNU. - ---- - -## Architecture Notes - -**Internal QueryClient** — The widget manages its own React Query `QueryClient`. You do not need to wrap it in a `QueryClientProvider`. - -**Wagmi Isolation** — In external wallet mode, the widget creates its own isolated read-only wagmi config for balance fetching. It does not interfere with your application's `WagmiProvider`. - -**AppKit Singleton** — In built-in wallet mode (`enableWalletConnection=true`), the widget uses Reown AppKit which is a page-level singleton. Only one AppKit instance can exist per page. If your dApp already uses AppKit or Web3Modal, you **must** use external wallet mode instead. - -**CSS Isolation** — All widget styles are prefixed with `ssw-` to avoid conflicts with host page styles. Import the stylesheet explicitly: - -```tsx -import '@shapeshiftoss/swap-widget/style.css' -``` +For the complete props reference, theming options, supported chains and swappers, and exported utilities/hooks, see the [package README](https://github.com/shapeshift/web/blob/develop/packages/swap-widget/README.md). diff --git a/packages/public-api/src/lib/quoteStore.ts b/packages/public-api/src/lib/quoteStore.ts index 46504a9fa66..60d8d6a2615 100644 --- a/packages/public-api/src/lib/quoteStore.ts +++ b/packages/public-api/src/lib/quoteStore.ts @@ -10,6 +10,7 @@ export type StoredQuote = { sendAddress: string receiveAddress: string partnerAddress?: string + partnerCode?: string partnerBps?: string shapeshiftBps: string affiliateBps: string diff --git a/packages/public-api/src/routes/affiliate/getAffiliateStats.ts b/packages/public-api/src/routes/affiliate/getAffiliateStats.ts index 6b76a1ecc4e..4572a1df015 100644 --- a/packages/public-api/src/routes/affiliate/getAffiliateStats.ts +++ b/packages/public-api/src/routes/affiliate/getAffiliateStats.ts @@ -14,7 +14,7 @@ registry.registerPath({ operationId: 'getAffiliateStats', summary: 'Get affiliate statistics', description: - 'Retrieve aggregated swap statistics for an affiliate address. Returns total swaps, volume, and fees earned. Supports optional date range filtering.', + 'Retrieve aggregated swap statistics for an affiliate by partnerCode. Returns total swaps, volume, and fees earned. Supports optional date range filtering.', tags: ['Affiliate'], request: { query: AffiliateStatsRequestSchema, @@ -44,10 +44,10 @@ export const getAffiliateStats = async (req: Request, res: Response): Promise => { receiveAddress, sendAddress, partnerAddress: req.affiliateInfo?.partnerAddress, + partnerCode: req.affiliateInfo?.partnerCode, createdAt: now, expiresAt: now + QuoteStore.QUOTE_TTL_MS, metadata: { diff --git a/packages/public-api/src/routes/status/utils.ts b/packages/public-api/src/routes/status/utils.ts index 606a0126fb0..bbfc7e67051 100644 --- a/packages/public-api/src/routes/status/utils.ts +++ b/packages/public-api/src/routes/status/utils.ts @@ -22,6 +22,7 @@ const buildSwapRegistrationBody = (storedQuote: ReturnType **This README is the canonical reference for the swap widget.** Other docs (including the ShapeShift Public API docs) link here. ## Table of Contents - [Installation](#installation) - [Quick Start](#quick-start) +- [Wallet Connection](#wallet-connection) - [Props Reference](#props-reference) +- [Filtering Chains and Assets](#filtering-chains-and-assets) - [Theming](#theming) - [Examples](#examples) - [Exported Types](#exported-types) - [Exported Utilities](#exported-utilities) - [Exported Hooks](#exported-hooks) - [Supported Chains](#supported-chains) +- [Supported Swappers](#supported-swappers) +- [Partner Codes & Affiliate Revenue](#partner-codes--affiliate-revenue) - [Notes and Limitations](#notes-and-limitations) ## Installation ```bash -pnpm add @shapeshiftoss/swap-widget -# or npm install @shapeshiftoss/swap-widget ``` ### Peer Dependencies -This package requires React 18 or later: +The widget relies on React, wagmi/viem, React Query, and Reown AppKit (used internally for wallet +connection). The widget initializes AppKit with the EVM, Bitcoin, and Solana adapters at load, so all +of these peers are required — install them alongside the package: -```json -{ - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } -} +```bash +npm install react react-dom \ + wagmi @wagmi/core viem \ + @tanstack/react-query \ + @reown/appkit @reown/appkit-adapter-wagmi \ + @reown/appkit-adapter-bitcoin @reown/appkit-adapter-solana \ + @solana/wallet-adapter-wallets @solana/web3.js +``` + +- **React 18 or 19** is supported (`^18.0.0 || ^19.0.0`). + +### Import the stylesheet + +The widget ships a stylesheet that **must** be imported once for it to render correctly: + +```tsx +import '@shapeshiftoss/swap-widget/style.css' ``` ## Quick Start ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; +import '@shapeshiftoss/swap-widget/style.css' + +import { SwapWidget } from '@shapeshiftoss/swap-widget' function App() { return ( console.log("Success:", txHash)} - onSwapError={(error) => console.error("Error:", error)} + onSwapSuccess={txHash => console.log('Success:', txHash)} + onSwapError={error => console.error('Error:', error)} /> - ); + ) } ``` -## Props Reference +## Wallet Connection -### SwapWidgetProps - -| Prop | Type | Default | Description | -| ------------------------ | ----------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- | -| `partnerCode` | `string` | - | Your registered partner code for affiliate fee attribution. Register at the affiliate dashboard. | -| `apiBaseUrl` | `string` | - | Custom API base URL. Useful for testing or custom deployments. | -| `defaultSellAsset` | `Asset` | ETH on Ethereum | Initial asset to sell. | -| `defaultBuyAsset` | `Asset` | USDC on Ethereum | Initial asset to buy. | -| `disabledChainIds` | `ChainId[]` | `[]` | Chain IDs to hide from the asset selector. | -| `disabledAssetIds` | `AssetId[]` | `[]` | Asset IDs to hide from the asset selector. | -| `allowedChainIds` | `ChainId[]` | - | If provided, only show assets from these chains. Use this to restrict the widget to specific chains. | -| `allowedAssetIds` | `AssetId[]` | - | If provided, only show these specific assets. | -| `walletClient` | `WalletClient` | - | Viem wallet client for executing EVM transactions. | -| `onConnectWallet` | `() => void` | - | Callback when user clicks "Connect Wallet" button. | -| `onSwapSuccess` | `(txHash: string) => void` | - | Callback when a swap transaction succeeds. | -| `onSwapError` | `(error: Error) => void` | - | Callback when a swap transaction fails. | -| `onAssetSelect` | `(type: "sell" \| "buy", asset: Asset) => void` | - | Callback when user selects an asset. | -| `theme` | `ThemeMode \| ThemeConfig` | `"dark"` | Theme mode (`"light"` or `"dark"`) or full theme configuration. | -| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance percentage. | -| `showPoweredBy` | `boolean` | `true` | Show "Powered by ShapeShift" branding. | -| `enableWalletConnection` | `boolean` | `false` | Enable built-in wallet connection UI using Reown AppKit. Supports EVM, Bitcoin, and Solana wallets. Requires `walletConnectProjectId`. | -| `walletConnectProjectId` | `string` | - | WalletConnect project ID for the built-in wallet connection. Get one at . | -| `defaultReceiveAddress` | `string` | - | Fixed receive address for swaps. When set, users cannot change the receive address. | +The widget connects wallets through [Reown AppKit](https://reown.com/appkit) +and provides the `WagmiProvider` / `QueryClient` it needs — you don't wrap it in your own. -## Theming +### Initializing AppKit -The widget supports both simple theme modes and full customization. +**The widget renders nothing until AppKit is initialized.** There are two ways to satisfy this: -### Simple Theme Mode +- **Let the widget initialize AppKit (default).** Pass `walletConnectProjectId` and the widget creates + and owns its own AppKit instance. Get a free project ID at . -```tsx - -// or - -``` + ```tsx + + ``` -### Custom Theme Configuration +- **Reuse your app's existing AppKit.** If your host app already calls `createAppKit()` (with a wagmi + EVM adapter), omit `walletConnectProjectId`. The widget detects the shared AppKit singleton, reads the + wagmi config off it, and provides its own `WagmiProvider` / `QueryClient` from that config — **you wrap + the widget in no providers of your own.** Pair this with `showConnectButton={false}` to drive + connection entirely from your own UI. See `src/demo/ExternalWalletApp.tsx` for a full host example. -```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; -import type { ThemeConfig } from "@shapeshiftoss/swap-widget"; + Two requirements for this mode: -const customTheme: ThemeConfig = { - mode: "dark", - accentColor: "#3861fb", // Primary accent color (buttons, focus states) - backgroundColor: "#0a0a14", // Widget background - cardColor: "#12121c", // Card/panel background - textColor: "#ffffff", // Primary text color - borderRadius: "12px", // Border radius for elements - fontFamily: "Inter, sans-serif", -}; + - **Initialize AppKit _before_ the widget mounts.** The widget reads the AppKit singleton when it + mounts and does not poll for late initialization — if AppKit isn't up yet, the widget renders nothing. + - **Dedupe the AppKit/wagmi packages.** `@reown/appkit*`, `wagmi`, and `viem` must resolve to a single + shared copy in your app, so the widget and your app share one AppKit instance and one wagmi state. A + duplicated copy means the widget reads an empty store and shows no connection. -function App() { - return ; -} -``` +The header shows a built-in **Connect** button by default; set `showConnectButton={false}` to hide it +and drive connection from your own UI. -### ThemeConfig Properties +### Supported wallet namespaces -| Property | Type | Description | -| ----------------- | ------------------- | -------------------------------------------------- | -| `mode` | `"light" \| "dark"` | Base theme mode. Required. | -| `accentColor` | `string` | Primary accent color for buttons and focus states. | -| `backgroundColor` | `string` | Widget background color. | -| `cardColor` | `string` | Card and panel background color. | -| `textColor` | `string` | Primary text color. | -| `borderRadius` | `string` | Border radius for UI elements. | -| `fontFamily` | `string` | Font family for the widget. | +Once connected, the widget can sign and broadcast transactions for three wallet namespaces: -## Examples +| Namespace | Chains | Example wallets | +| --------- | ------------------------------- | -------------------------------- | +| `eip155` | All supported EVM chains | MetaMask, WalletConnect, Rabby | +| `bip122` | Bitcoin and other UTXO chains | WalletConnect-compatible wallets | +| `solana` | Solana | Phantom, Solflare | -### Basic Usage +The header shows a **Connect** button by default (toggle with `showConnectButton`) that opens the +AppKit modal. Swaps whose sell asset is not in an executable namespace (see +[Supported Chains](#supported-chains)) redirect to [app.shapeshift.com](https://app.shapeshift.com) +when `allowShapeshiftRedirect` is enabled. -```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; +## Props Reference -function App() { - return ; +### `SwapWidgetProps` + +| Prop | Type | Default | Description | +| ------------------------ | ----------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------- | +| `walletConnectProjectId` | `string` | – | Reown AppKit / WalletConnect project ID. The widget uses it to initialize AppKit. Required unless your host app already initializes AppKit (see [Wallet Connection](#wallet-connection)). | +| `partnerCode` | `string` | – | Your registered partner code for affiliate fee attribution. See [Partner Codes](#partner-codes--affiliate-revenue). | +| `apiBaseUrl` | `string` | `https://api.shapeshift.com` | Override the API base URL. Useful for testing or custom deployments. | +| `defaultSellAsset` | `Asset` | ETH on Ethereum | Initial asset to sell. | +| `defaultBuyAsset` | `Asset` | USDC on Ethereum | Initial asset to buy. | +| `sellFilters` | `SwapWidgetFilters` | `{}` | Restrict which chains/assets are selectable for the **sell** side. See [Filtering](#filtering-chains-and-assets). | +| `buyFilters` | `SwapWidgetFilters` | `{}` | Restrict which chains/assets are selectable for the **buy** side. | +| `allowedSwapperNames` | `SwapperName[]` | all enabled | Limit quotes to specific swappers. See [Supported Swappers](#supported-swappers). | +| `allowShapeshiftRedirect`| `boolean` | `true` | When a swap isn't executable in-widget, redirect to app.shapeshift.com instead of hiding it. | +| `isBuyAssetLocked` | `boolean` | `false` | Prevent the user from changing the buy asset. | +| `theme` | `ThemeMode \| ThemeConfig` | `"dark"` | Theme mode (`"light"` or `"dark"`) or a full theme configuration object. See [Theming](#theming). | +| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance, as a percentage string. | +| `showPoweredBy` | `boolean` | `true` | Show the "Powered by ShapeShift" footer. | +| `showConnectButton` | `boolean` | `true` | Show the built-in Connect button in the widget header. | +| `ratesRefetchInterval` | `number` | `15000` | How often (ms) to refetch swap rates. | +| `onSwapSuccess` | `(txHash: string) => void` | – | Called when a swap transaction succeeds. | +| `onSwapError` | `(error: Error) => void` | – | Called when a swap transaction fails. | + +## Filtering Chains and Assets + +Restrict the sell and/or buy asset selectors independently via the `sellFilters` and `buyFilters` +props. Both accept the same shape: + +```typescript +type SwapWidgetFilters = { + allowedChainIds?: ChainId[] // If set, only these chains are selectable + disabledChainIds?: ChainId[] // Hide these chains + allowedAssetIds?: AssetId[] // If set, only these assets are selectable + disabledAssetIds?: AssetId[] // Hide these assets } ``` -### With External Wallet Connection (wagmi/viem) - -If you already have wagmi set up in your application, you can pass the wallet client directly: - ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; -import { useWalletClient } from "wagmi"; +import { EVM_CHAIN_IDS, SwapWidget } from '@shapeshiftoss/swap-widget' function App() { - const { data: walletClient } = useWalletClient(); - return ( { - // Your custom wallet connection logic - }} - onSwapSuccess={(txHash) => { - console.log("Swap successful:", txHash); - }} - onSwapError={(error) => { - console.error("Swap failed:", error); + // Only allow selling ETH-chain, Polygon, and Arbitrum assets + sellFilters={{ + allowedChainIds: [EVM_CHAIN_IDS.ethereum, EVM_CHAIN_IDS.polygon, EVM_CHAIN_IDS.arbitrum], }} - theme={{ - mode: "dark", - accentColor: "#3861fb", - backgroundColor: "#0a0a14", - cardColor: "#12121c", + // Hide a specific buy token + buyFilters={{ + disabledAssetIds: ['eip155:1/erc20:0x...'], }} + theme="dark" /> - ); + ) } ``` -### With Custom Default Assets +## Theming + +The widget supports a simple light/dark mode or a full theme configuration object. + +### Simple theme mode ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; -import type { Asset } from "@shapeshiftoss/swap-widget"; + +// or + +``` -const defaultSellAsset: Asset = { - assetId: "eip155:137/slip44:966", - chainId: "eip155:137", - symbol: "MATIC", - name: "Polygon", - precision: 18, - icon: "https://example.com/matic.png", -}; +### Custom theme configuration -const defaultBuyAsset: Asset = { - assetId: "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", - chainId: "eip155:137", - symbol: "USDC", - name: "USD Coin", - precision: 6, - icon: "https://example.com/usdc.png", -}; +```tsx +import { SwapWidget } from '@shapeshiftoss/swap-widget' +import type { ThemeConfig } from '@shapeshiftoss/swap-widget' + +const customTheme: ThemeConfig = { + mode: 'dark', // required + accentColor: '#3861fb', + backgroundColor: '#0a0a14', + cardColor: '#12121c', + textColor: '#ffffff', + borderRadius: '12px', + fontFamily: 'Inter, sans-serif', + buttonVariant: 'filled', +} function App() { - return ( - - ); + return } ``` -### Restricting Available Chains and Assets +### `ThemeConfig` properties + +| Property | Type | Description | +| -------------------- | ------------------------ | ---------------------------------------------------- | +| `mode` | `"light" \| "dark"` | Base theme mode. **Required.** | +| `accentColor` | `string` | Primary accent color (buttons, focus states). | +| `backgroundColor` | `string` | Widget background color. | +| `cardColor` | `string` | Card / panel background color. | +| `textColor` | `string` | Primary text color. | +| `secondaryTextColor` | `string` | Secondary text color. | +| `mutedTextColor` | `string` | Muted/tertiary text color. | +| `inputColor` | `string` | Input field background color. | +| `hoverColor` | `string` | Hover background color. | +| `borderColor` | `string` | Border color. | +| `borderRadius` | `string` | Base border radius for UI elements (e.g. `"12px"`). | +| `fontFamily` | `string` | Font family for the widget. | +| `buttonVariant` | `"filled" \| "outline"` | Primary button style. | -Use `allowedChainIds` to restrict the widget to only show specific chains. This is useful when you want to limit swaps to certain networks. +## Examples + +### Basic usage ```tsx -import { SwapWidget, EVM_CHAIN_IDS } from "@shapeshiftoss/swap-widget"; +import { SwapWidget } from '@shapeshiftoss/swap-widget' function App() { - return ( - - ); + return } ``` -### With Built-in Wallet Connection (Multi-Chain) - -The widget can manage wallet connections internally using Reown AppKit, which supports EVM chains, Bitcoin, and Solana. This is useful when you don't have an existing wallet connection setup. +### Custom default assets ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; +import { SwapWidget } from '@shapeshiftoss/swap-widget' +import type { Asset } from '@shapeshiftoss/swap-widget' + +const defaultSellAsset: Asset = { + assetId: 'eip155:137/slip44:966', + chainId: 'eip155:137', + symbol: 'POL', + name: 'Polygon', + precision: 18, +} + +const defaultBuyAsset: Asset = { + assetId: 'eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + chainId: 'eip155:137', + symbol: 'USDC', + name: 'USD Coin', + precision: 6, +} function App() { return ( - ); + ) } ``` -When `enableWalletConnection` is true, the widget will: -- Show a "Connect" button that opens the AppKit modal -- Support connecting EVM wallets (MetaMask, WalletConnect, etc.) -- Support connecting Bitcoin wallets via WalletConnect -- Support connecting Solana wallets (Phantom, Solflare, etc.) +### Locking the buy asset -### With Fixed Receive Address - -Use `defaultReceiveAddress` to lock the receive address. When set, users cannot change the destination address. This is useful for integrations where you want all swaps to go to a specific address. +Use `isBuyAssetLocked` so users can only change the sell side — useful when you want all swaps to +end in a specific token. ```tsx -import { SwapWidget } from "@shapeshiftoss/swap-widget"; - -function App() { - return ( - - ); -} + ``` ## Exported Types @@ -281,246 +303,224 @@ function App() { import type { Asset, AssetId, - ChainId, Chain, - TradeRate, - TradeQuote, - SwapperName, + ChainId, + SwapWidgetFilters, SwapWidgetProps, - ThemeMode, ThemeConfig, -} from "@shapeshiftoss/swap-widget"; + ThemeMode, + TradeQuote, + TradeRate, +} from '@shapeshiftoss/swap-widget' ``` -### Asset - -```typescript -type Asset = { - assetId: AssetId; // CAIP-19 format: "eip155:1/slip44:60" - chainId: ChainId; // CAIP-2 format: "eip155:1" - symbol: string; // e.g., "ETH" - name: string; // e.g., "Ethereum" - precision: number; // e.g., 18 - icon?: string; // URL to asset icon - color?: string; // Brand color - networkName?: string; // Display name for the network - networkIcon?: string; // URL to network icon - explorer?: string; // Block explorer URL - explorerTxLink?: string; // Transaction explorer link template - explorerAddressLink?: string; // Address explorer link template - relatedAssetKey?: AssetId | null; // Related asset for bridged tokens -}; -``` +`SwapperName` is exported as a runtime value (an `enum`) — import it from the value position, not as +a type. -### SwapperName +### `Asset` ```typescript -type SwapperName = - | "THORChain" - | "MAYAChain" - | "CoW Swap" - | "0x" - | "Portals" - | "Chainflip" - | "Relay" - | "Bebop" - | "Jupiter" - | "1inch" - | "ButterSwap" - | "ArbitrumBridge"; -``` - -### TradeRate - -```typescript -type TradeRate = { - swapperName: SwapperName; - rate: string; - buyAmountCryptoBaseUnit: string; - sellAmountCryptoBaseUnit: string; - steps: number; - estimatedExecutionTimeMs?: number; - affiliateBps: string; - networkFeeCryptoBaseUnit?: string; - error?: { - code: string; - message: string; - }; - id?: string; -}; +type Asset = { + assetId: AssetId // CAIP-19, e.g. "eip155:1/slip44:60" + chainId: ChainId // CAIP-2, e.g. "eip155:1" + symbol: string // e.g. "ETH" + name: string // e.g. "Ethereum" + precision: number // e.g. 18 + icon?: string + color?: string + networkName?: string + networkIcon?: string + explorer?: string + explorerTxLink?: string + explorerAddressLink?: string + relatedAssetKey?: AssetId | null +} ``` ## Exported Utilities ```typescript import { - isEvmChainId, - getEvmChainIdNumber, - getChainType, - formatAmount, - parseAmount, - truncateAddress, - EVM_CHAIN_IDS, - UTXO_CHAIN_IDS, COSMOS_CHAIN_IDS, + EVM_CHAIN_IDS, OTHER_CHAIN_IDS, - CHAIN_METADATA, - getChainMeta, - getChainName, - getChainIcon, + REDIRECT_ONLY_CHAIN_IDS, + SwapperName, + UTXO_CHAIN_IDS, + formatAmount, + getBaseAsset, getChainColor, -} from "@shapeshiftoss/swap-widget"; + getChainIcon, + getChainName, + getChainType, + getEvmNetworkId, + getExplorerTxLink, + isEvmChainId, + isWidgetExecutableChainId, + isWidgetSupportedChainId, + parseAmount, + truncateAddress, +} from '@shapeshiftoss/swap-widget' ``` -### Chain Type Utilities - -| Function | Signature | Description | -| --------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------- | -| `isEvmChainId` | `(chainId: string) => boolean` | Check if a chain ID is an EVM chain. | -| `getEvmChainIdNumber` | `(chainId: string) => number` | Extract the numeric chain ID from a CAIP-2 chain ID. | -| `getChainType` | `(chainId: string) => "evm" \| "utxo" \| "cosmos" \| "solana" \| "other"` | Get the chain type from a chain ID. | +### Chain helpers -### Amount Formatting +| Function | Signature | Description | +| --------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `isEvmChainId` | `(chainId: string) => boolean` | Whether a chain ID is an EVM chain. | +| `getEvmNetworkId` | `(chainId: string) => number` | Extract the numeric network ID from a CAIP-2 EVM chain ID. | +| `getChainType` | `(chainId: string) => "evm" \| "utxo" \| "cosmos" \| "solana" \| "other"` | Classify a chain by namespace. | +| `isWidgetSupportedChainId` | `(chainId: string) => boolean` | Whether the widget lists assets on this chain. | +| `isWidgetExecutableChainId` | `(chainId: string) => boolean` | Whether the widget can sign/execute swaps on this chain in-app. | +| `getChainName` | `(chainId: ChainId) => string` | Display name for a chain. | +| `getChainIcon` | `(chainId: ChainId) => string \| undefined` | Icon URL for a chain. | +| `getChainColor` | `(chainId: ChainId) => string` | Brand color for a chain. | +| `getBaseAsset` | `(chainId: ChainId) => Asset \| undefined` | Native asset for a chain. | +| `getExplorerTxLink` | `(chainId: ChainId) => string \| undefined` | Block-explorer transaction link template. | -| Function | Signature | Description | -| ----------------- | -------------------------------------------------------------------- | -------------------------------------------------------- | -| `formatAmount` | `(amount: string, decimals: number, maxDecimals?: number) => string` | Format a base unit amount for display. | -| `parseAmount` | `(amount: string, decimals: number) => string` | Parse a human-readable amount to base units. | -| `truncateAddress` | `(address: string, chars?: number) => string` | Truncate an address for display (e.g., `0x1234...5678`). | +### Amount and address formatting -### Chain Metadata +| Function | Signature | Description | +| ----------------- | -------------------------------------------------------------------- | ---------------------------------------------------- | +| `formatAmount` | `(amount: string, decimals: number, maxDecimals?: number) => string` | Format a base-unit amount for display. | +| `parseAmount` | `(amount: string, decimals: number) => string` | Parse a human-readable amount into base units. | +| `truncateAddress` | `(address: string, chars?: number) => string` | Truncate an address (e.g. `0x1234...5678`). | -| Function | Signature | Description | -| --------------- | ---------------------------------------------- | --------------------------------- | -| `getChainMeta` | `(chainId: ChainId) => ChainMeta \| undefined` | Get full metadata for a chain. | -| `getChainName` | `(chainId: ChainId) => string` | Get the display name for a chain. | -| `getChainIcon` | `(chainId: ChainId) => string \| undefined` | Get the icon URL for a chain. | -| `getChainColor` | `(chainId: ChainId) => string` | Get the brand color for a chain. | - -### Chain ID Constants +### Chain ID constants ```typescript const EVM_CHAIN_IDS = { - ethereum: "eip155:1", - arbitrum: "eip155:42161", - optimism: "eip155:10", - polygon: "eip155:137", - base: "eip155:8453", - avalanche: "eip155:43114", - bsc: "eip155:56", - gnosis: "eip155:100", -}; + ethereum: 'eip155:1', + arbitrum: 'eip155:42161', + optimism: 'eip155:10', + polygon: 'eip155:137', + base: 'eip155:8453', + avalanche: 'eip155:43114', + bsc: 'eip155:56', + gnosis: 'eip155:100', + monad: 'eip155:143', + megaEth: 'eip155:4326', + hyperEvm: 'eip155:999', + plasma: 'eip155:9745', + katana: 'eip155:747474', +} const UTXO_CHAIN_IDS = { - bitcoin: "bip122:000000000019d6689c085ae165831e93", - bitcoinCash: "bip122:000000000000000000651ef99cb9fcbe", - dogecoin: "bip122:00000000001a91e3dace36e2be3bf030", - litecoin: "bip122:12a765e31ffd4059bada1e25190f6e98", -}; + bitcoin: 'bip122:000000000019d6689c085ae165831e93', + bitcoinCash: 'bip122:000000000000000000651ef99cb9fcbe', + dogecoin: 'bip122:00000000001a91e3dace36e2be3bf030', + litecoin: 'bip122:12a765e31ffd4059bada1e25190f6e98', +} const COSMOS_CHAIN_IDS = { - cosmos: "cosmos:cosmoshub-4", - thorchain: "cosmos:thorchain-1", - mayachain: "cosmos:mayachain-mainnet-v1", -}; + cosmos: 'cosmos:cosmoshub-4', + thorchain: 'cosmos:thorchain-1', + mayachain: 'cosmos:mayachain-mainnet-v1', +} const OTHER_CHAIN_IDS = { - solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", -}; + solana: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', +} + +// Listed in the asset selector, but swaps redirect to app.shapeshift.com (not executed in-widget) +const REDIRECT_ONLY_CHAIN_IDS = { + zcash: 'bip122:00040fe8ec8471911baa1db1266ea15d', + tron: 'tron:0x2b6653dc', + sui: 'sui:35834a8a', + ton: 'ton:mainnet', + near: 'near:mainnet', + starknet: 'starknet:SN_MAIN', +} ``` ## Exported Hooks ```typescript import { - useAssets, useAssetById, - useChains, - useAssetsByChainId, useAssetSearch, -} from "@shapeshiftoss/swap-widget"; + useAssets, + useAssetsByChainId, + useChains, +} from '@shapeshiftoss/swap-widget' ``` -| Hook | Return Type | Description | -| --------------------------------- | ------------------------------------------ | -------------------------------------------------------------- | -| `useAssets()` | `{ data: Asset[], isLoading, error, ... }` | Fetch all available assets. | -| `useAssetById(assetId)` | `{ data: Asset \| undefined, ... }` | Fetch a specific asset by ID. | -| `useChains()` | `{ data: ChainInfo[], ... }` | Fetch all available chains with their native assets. | -| `useAssetsByChainId(chainId)` | `{ data: Asset[], ... }` | Fetch all assets for a specific chain. | -| `useAssetSearch(query, chainId?)` | `{ data: Asset[], ... }` | Search assets by symbol or name, optionally filtered by chain. | - -## Supported Chains - -| Chain | Chain ID | Type | -| ----------------- | ----------------------------------------- | ------ | -| Ethereum | `eip155:1` | EVM | -| Arbitrum One | `eip155:42161` | EVM | -| Optimism | `eip155:10` | EVM | -| Polygon | `eip155:137` | EVM | -| Base | `eip155:8453` | EVM | -| Avalanche C-Chain | `eip155:43114` | EVM | -| BNB Smart Chain | `eip155:56` | EVM | -| Gnosis | `eip155:100` | EVM | -| Bitcoin | `bip122:000000000019d6689c085ae165831e93` | UTXO | -| Bitcoin Cash | `bip122:000000000000000000651ef99cb9fcbe` | UTXO | -| Dogecoin | `bip122:00000000001a91e3dace36e2be3bf030` | UTXO | -| Litecoin | `bip122:12a765e31ffd4059bada1e25190f6e98` | UTXO | -| Cosmos Hub | `cosmos:cosmoshub-4` | Cosmos | -| THORChain | `cosmos:thorchain-1` | Cosmos | -| MAYAChain | `cosmos:mayachain-mainnet-v1` | Cosmos | -| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Solana | - -## Notes and Limitations - -### Multi-Chain Swap Support +| Hook | Description | +| --------------------------------- | -------------------------------------------------------------- | +| `useAssets()` | Fetch all available assets. | +| `useAssetById(assetId)` | Fetch a single asset by ID. | +| `useChains()` | Fetch all available chains with their native assets. | +| `useAssetsByChainId(chainId)` | Fetch all assets for a specific chain. | +| `useAssetSearch(query, chainId?)` | Search assets by symbol or name, optionally filtered by chain. | -The widget supports swaps across multiple blockchain types: +These hooks must be used within a mounted `` tree (they rely on the widget's internal +React Query client). -- **EVM swaps** (e.g., ETH to USDC, MATIC to WETH) can be executed directly within the widget when a wallet is connected via the `walletClient` prop or through the built-in AppKit wallet connection. -- **Bitcoin/UTXO swaps** - When using the built-in wallet connection (`enableWalletConnection={true}`), Bitcoin and other UTXO chains can be signed directly via WalletConnect-compatible wallets. -- **Solana swaps** - Solana transactions can be signed via Phantom, Solflare, or other Solana wallets when using the built-in wallet connection. -- **Unsupported chains** - Swaps involving chains without wallet support will redirect to [app.shapeshift.com](https://app.shapeshift.com) to complete the transaction. - -### Partner Codes +## Supported Chains -Register an affiliate account at the affiliate dashboard and claim a partner code (e.g. `vultisig`, `venice`). Pass it via the `partnerCode` prop on the widget, or the `X-Partner-Code` header when using the REST API directly: +Assets on the following chains appear in the selector. Swaps are **executed in-widget** only for EVM, +UTXO, and Solana assets (`isWidgetExecutableChainId` returns `true`). Cosmos-SDK and redirect-only +chains are selectable but route the user to [app.shapeshift.com](https://app.shapeshift.com) to +complete the swap (when `allowShapeshiftRedirect` is enabled). + +| Chain | Chain ID | Type | Executable in-widget | +| ----------------- | ----------------------------------------- | ------ | -------------------- | +| Ethereum | `eip155:1` | EVM | ✅ | +| Arbitrum One | `eip155:42161` | EVM | ✅ | +| Optimism | `eip155:10` | EVM | ✅ | +| Polygon | `eip155:137` | EVM | ✅ | +| Base | `eip155:8453` | EVM | ✅ | +| Avalanche C-Chain | `eip155:43114` | EVM | ✅ | +| BNB Smart Chain | `eip155:56` | EVM | ✅ | +| Gnosis | `eip155:100` | EVM | ✅ | +| Monad | `eip155:143` | EVM | ✅ | +| MegaETH | `eip155:4326` | EVM | ✅ | +| HyperEVM | `eip155:999` | EVM | ✅ | +| Plasma | `eip155:9745` | EVM | ✅ | +| Katana | `eip155:747474` | EVM | ✅ | +| Bitcoin | `bip122:000000000019d6689c085ae165831e93` | UTXO | ✅ | +| Bitcoin Cash | `bip122:000000000000000000651ef99cb9fcbe` | UTXO | ✅ | +| Dogecoin | `bip122:00000000001a91e3dace36e2be3bf030` | UTXO | ✅ | +| Litecoin | `bip122:12a765e31ffd4059bada1e25190f6e98` | UTXO | ✅ | +| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Solana | ✅ | +| Cosmos Hub | `cosmos:cosmoshub-4` | Cosmos | ↗ redirect | +| THORChain | `cosmos:thorchain-1` | Cosmos | ↗ redirect | +| MAYAChain | `cosmos:mayachain-mainnet-v1` | Cosmos | ↗ redirect | +| Zcash, Tron, Sui, TON, NEAR, Starknet | _see `REDIRECT_ONLY_CHAIN_IDS`_ | Other | ↗ redirect | + +## Supported Swappers + +The widget aggregates quotes across the protocols below and surfaces the best rate. Use +`allowedSwapperNames` to restrict which are used. + +- **NEAR Intents** (`SwapperName.NearIntents`) +- **Relay** (`SwapperName.Relay`) +- **THORChain** (`SwapperName.Thorchain`) +- **MAYAChain** (`SwapperName.Mayachain`) + +> The set of enabled swappers changes over time. Treat this list as current-at-publish; the +> authoritative source is the `SwapperName` enum exported by this package. + +## Partner Codes & Affiliate Revenue + +Pass your registered `partnerCode` to attribute swaps to your affiliate account and earn revenue +share. The widget forwards it to the ShapeShift Public API as the `X-Partner-Code` header, and the +API applies your configured fee automatically. +```tsx + ``` -GET /v1/swap/rates?... -X-Partner-Code: your-partner-code -``` - -The API resolves the partner code to the registered affiliate address and BPS automatically. -### Internal QueryClient +See the [Affiliate Program guide](../../docs/affiliates.md) for how to obtain a partner code and how +revenue attribution works. -The widget manages its own React Query `QueryClient` internally. You do not need to wrap it in a `QueryClientProvider`. - -### Swap Aggregation - -The widget fetches quotes from multiple DEXs and aggregators including: - -- THORChain -- MAYAChain -- CoW Swap -- 0x -- 1inch -- Portals -- Chainflip -- Jupiter (Solana) -- Bebop -- Relay -- ButterSwap -- Arbitrum Bridge - -### Wallet Balance Display - -When a wallet is connected (`walletClient` prop), the widget displays the user's balance for the selected sell and buy assets. This only works for EVM chains where the connected wallet has assets. - -### USD Price Display - -The widget automatically fetches and displays USD prices for selected assets. - -### Mobile Responsive +## Notes and Limitations -The widget is designed to be responsive and works well on mobile devices. +- **Self-contained providers.** The widget renders its own `WagmiProvider` and React Query + `QueryClient`. Don't wrap it in your own — and remember it renders nothing until AppKit is + initialized, whether by `walletConnectProjectId` or by your host app (see [Wallet Connection](#wallet-connection)). +- **Balances and USD prices.** When a wallet is connected, the widget shows balances and USD prices + for the selected assets. +- **Redirects.** Assets on non-executable chains (Cosmos, Zcash, Tron, Sui, TON, NEAR, Starknet) + send the user to app.shapeshift.com to finish the swap, unless `allowShapeshiftRedirect={false}`. +- **Mobile responsive.** The widget is designed to work on mobile as well as desktop. diff --git a/packages/swap-widget/package.json b/packages/swap-widget/package.json index 757e641b7cc..63acc309628 100644 --- a/packages/swap-widget/package.json +++ b/packages/swap-widget/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/swap-widget", - "version": "0.4.0", + "version": "0.5.0", "description": "Embeddable swap widget using ShapeShift API", "repository": "https://github.com/shapeshift/web", "license": "MIT", diff --git a/packages/swap-widget/src/components/SwapWidget.tsx b/packages/swap-widget/src/components/SwapWidget.tsx index cd617d75db4..180802c5d8b 100644 --- a/packages/swap-widget/src/components/SwapWidget.tsx +++ b/packages/swap-widget/src/components/SwapWidget.tsx @@ -42,6 +42,7 @@ type SwapWidgetContentProps = { sellFilters: SwapWidgetFilters buyFilters: SwapWidgetFilters allowedSwapperNames?: SwapWidgetProps['allowedSwapperNames'] + ratesRefetchInterval?: SwapWidgetProps['ratesRefetchInterval'] } const SwapWidgetContent = ({ @@ -57,6 +58,7 @@ const SwapWidgetContent = ({ sellFilters, buyFilters, allowedSwapperNames, + ratesRefetchInterval, }: SwapWidgetContentProps) => { const state = SwapMachineCtx.useSelector(s => s) @@ -66,7 +68,11 @@ const SwapWidgetContent = ({ const themeMode: ThemeMode = typeof theme === 'string' ? theme : theme.mode const themeConfig = typeof theme === 'object' ? theme : undefined - const displayValues = useSwapDisplayValues({ apiClient, allowedSwapperNames }) + const displayValues = useSwapDisplayValues({ + apiClient, + allowedSwapperNames, + ratesRefetchInterval, + }) const { rates, sellAssetBalance, refetchSellBalance, refetchBuyBalance } = displayValues const { @@ -247,6 +253,7 @@ type SwapWidgetCoreProps = { sellFilters: SwapWidgetFilters buyFilters: SwapWidgetFilters allowedSwapperNames?: SwapWidgetProps['allowedSwapperNames'] + ratesRefetchInterval?: SwapWidgetProps['ratesRefetchInterval'] } const SwapWidgetCore = ({ @@ -265,6 +272,7 @@ const SwapWidgetCore = ({ sellFilters, buyFilters, allowedSwapperNames, + ratesRefetchInterval, }: SwapWidgetCoreProps) => { const actorRef = SwapMachineCtx.useActorRef() @@ -383,6 +391,7 @@ const SwapWidgetCore = ({ sellFilters={sellFilters} buyFilters={buyFilters} allowedSwapperNames={allowedSwapperNames} + ratesRefetchInterval={ratesRefetchInterval} /> ) @@ -417,6 +426,7 @@ export const SwapWidget = (props: SwapWidgetProps) => { sellFilters={props.sellFilters ?? {}} buyFilters={props.buyFilters ?? {}} allowedSwapperNames={props.allowedSwapperNames} + ratesRefetchInterval={props.ratesRefetchInterval} /> diff --git a/packages/swap-widget/src/components/WalletProvider.tsx b/packages/swap-widget/src/components/WalletProvider.tsx index bb8f2b740e9..e11628476ff 100644 --- a/packages/swap-widget/src/components/WalletProvider.tsx +++ b/packages/swap-widget/src/components/WalletProvider.tsx @@ -1,11 +1,11 @@ import { useAppKit } from '@reown/appkit/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import type { ReactNode } from 'react' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import type { Config } from 'wagmi' import { WagmiProvider } from 'wagmi' -import { getWagmiAdapter, initializeAppKit, isAppKitInitialized } from '../config/appkit' +import { getActiveWagmiConfig, initializeAppKit, isAppKitInitialized } from '../config/appkit' import { useSwapWallet } from '../contexts/SwapWalletContext' import { truncateAddress } from '../types' @@ -17,20 +17,24 @@ type AppKitWalletProviderProps = { } export const AppKitWalletProvider = ({ projectId, children }: AppKitWalletProviderProps) => { - const [isReady, setIsReady] = useState(false) + // Seed from the SDK singleton so a host-initialized AppKit is picked up on the + // first render (no flash of null) when it was created before the widget mounts. + const [wagmiConfig, setWagmiConfig] = useState(() => + isAppKitInitialized() ? getActiveWagmiConfig() : undefined, + ) useEffect(() => { - if (projectId) initializeAppKit(projectId) - if (isAppKitInitialized()) setIsReady(true) + // Self-init only when no AppKit exists yet. If the host already called + // createAppKit, we hook into that shared instance rather than creating a second. + if (projectId && !isAppKitInitialized()) initializeAppKit(projectId) + if (isAppKitInitialized()) setWagmiConfig(getActiveWagmiConfig()) }, [projectId]) - const wagmiConfig = useMemo((): Config | undefined => { - if (!isReady) return undefined - return getWagmiAdapter()?.wagmiConfig as unknown as Config | undefined - }, [isReady]) - if (!wagmiConfig) return null + // We always own the WagmiProvider / QueryClient — built from the wagmi config we + // read off the shared AppKit singleton (whether self-init or host-owned). This is + // what lets a host integrate by calling createAppKit() alone, wrapping nothing. return ( {children} diff --git a/packages/swap-widget/src/config/appkit.ts b/packages/swap-widget/src/config/appkit.ts index 7df859965b6..f52b39d7b62 100644 --- a/packages/swap-widget/src/config/appkit.ts +++ b/packages/swap-widget/src/config/appkit.ts @@ -16,11 +16,12 @@ import { polygon, solana, } from '@reown/appkit/networks' -import { createAppKit } from '@reown/appkit/react' +import { createAppKit, modal } from '@reown/appkit/react' import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin' import { SolanaAdapter } from '@reown/appkit-adapter-solana/react' import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets' +import type { Config } from 'wagmi' const EVM_NETWORKS: readonly AppKitNetwork[] = [ mainnet, @@ -40,17 +41,28 @@ const EVM_NETWORKS: readonly AppKitNetwork[] = [ const ALL_NETWORKS: readonly AppKitNetwork[] = [...EVM_NETWORKS, bitcoin, solana] -let wagmiAdapter: WagmiAdapter | null = null -let appKitInitialized = false +// AppKit keeps a module-level `modal` singleton, set by `createAppKit` whether +// it's called by this widget (self-init) or by the host app. Reading it lets the +// widget detect — and hook into — an AppKit the host already initialized, as long +// as `@reown/appkit*` is deduped to a single shared copy in the consumer's tree. +export const isAppKitInitialized = (): boolean => modal !== undefined -export const getWagmiAdapter = (): WagmiAdapter | null => wagmiAdapter - -export const isAppKitInitialized = (): boolean => appKitInitialized +// The active wagmi Config, read off the AppKit singleton's EVM adapter. Works in +// both modes — self-init (we registered the adapter via createAppKit) and +// host-owned (the host's adapter). Because the widget can pull the shared config +// from the singleton, it builds its own WagmiProvider from it — so a host only +// needs to call `createAppKit()`, with no WagmiProvider/QueryClient wrapping. +export const getActiveWagmiConfig = (): Config | undefined => { + const evmAdapter = modal?.chainAdapters?.eip155 as WagmiAdapter | undefined + return evmAdapter?.wagmiConfig +} export const initializeAppKit = (projectId: string): void => { - if (appKitInitialized) return + // Already initialized — either by a previous call here, or by the host app. In + // the latter case we skip entirely and reuse the host's AppKit + wagmi config. + if (modal) return - wagmiAdapter = new WagmiAdapter({ + const wagmiAdapter = new WagmiAdapter({ networks: [...EVM_NETWORKS], projectId, }) @@ -68,6 +80,4 @@ export const initializeAppKit = (projectId: string): void => { send: false, }, }) - - appKitInitialized = true } diff --git a/packages/swap-widget/src/demo/ExternalWalletApp.tsx b/packages/swap-widget/src/demo/ExternalWalletApp.tsx index 8b090dc74e2..dd2c42ec65e 100644 --- a/packages/swap-widget/src/demo/ExternalWalletApp.tsx +++ b/packages/swap-widget/src/demo/ExternalWalletApp.tsx @@ -1,10 +1,31 @@ import './App.css' -import { useAppKit, useAppKitAccount } from '@reown/appkit/react' +import type { AppKitNetwork } from '@reown/appkit/networks' +import { + arbitrum, + avalanche, + base, + bitcoin, + bsc, + gnosis, + hyperEvm, + katana, + mainnet, + megaeth, + monad, + optimism, + plasma, + polygon, + solana, +} from '@reown/appkit/networks' +import { createAppKit, useAppKit, useAppKitAccount } from '@reown/appkit/react' +import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin' +import { SolanaAdapter } from '@reown/appkit-adapter-solana/react' +import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' +import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets' import { useCallback, useEffect, useMemo, useState } from 'react' import { SwapWidget } from '../components/SwapWidget' -import { initializeAppKit } from '../config/appkit' import { truncateAddress } from '../types' import { DemoCustomizer, useDemoTheme } from './DemoCustomizer' import { WidgetModal } from './WidgetModal' @@ -180,9 +201,54 @@ const loadThemeMode = (): 'light' | 'dark' => { } } +const EVM_NETWORKS: readonly AppKitNetwork[] = [ + mainnet, + polygon, + arbitrum, + optimism, + base, + avalanche, + bsc, + gnosis, + monad, + megaeth, + hyperEvm, + plasma, + katana, +] + +const ALL_NETWORKS: readonly AppKitNetwork[] = [...EVM_NETWORKS, bitcoin, solana] + +let appKitInitialized = false + +const initAppKit = (projectId: string): void => { + if (appKitInitialized) return + appKitInitialized = true + + const wagmiAdapter = new WagmiAdapter({ + networks: [...EVM_NETWORKS], + projectId, + }) + + const bitcoinAdapter = new BitcoinAdapter() + const solanaAdapter = new SolanaAdapter({ + wallets: [new PhantomWalletAdapter(), new SolflareWalletAdapter()] as any, + }) + + createAppKit({ + adapters: [wagmiAdapter, bitcoinAdapter, solanaAdapter], + projectId, + networks: [...ALL_NETWORKS] as [AppKitNetwork, ...AppKitNetwork[]], + features: { + send: false, + }, + }) +} + export const ExternalWalletApp = () => { const [theme, setTheme] = useState<'light' | 'dark'>(loadThemeMode) - const [isReady, setIsReady] = useState(false) + + useState(() => initAppKit(PROJECT_ID)) useEffect(() => { try { @@ -192,11 +258,5 @@ export const ExternalWalletApp = () => { } }, [theme]) - useEffect(() => { - initializeAppKit(PROJECT_ID) - setIsReady(true) - }, []) - - if (!isReady) return null return } diff --git a/packages/swap-widget/src/hooks/useSwapDisplayValues.ts b/packages/swap-widget/src/hooks/useSwapDisplayValues.ts index 99497a2d8e0..ca1a60783b2 100644 --- a/packages/swap-widget/src/hooks/useSwapDisplayValues.ts +++ b/packages/swap-widget/src/hooks/useSwapDisplayValues.ts @@ -18,6 +18,7 @@ import { useSwapRates } from './useSwapRates' type UseSwapDisplayValuesParams = { apiClient: ApiClient allowedSwapperNames?: SwapperName[] + ratesRefetchInterval?: number } export type SwapDisplayValues = { @@ -47,6 +48,7 @@ export type SwapDisplayValues = { export const useSwapDisplayValues = ({ apiClient, allowedSwapperNames, + ratesRefetchInterval, }: UseSwapDisplayValuesParams): SwapDisplayValues => { const sellAsset = SwapMachineCtx.useSelector(s => s.context.sellAsset) const buyAsset = SwapMachineCtx.useSelector(s => s.context.buyAsset) @@ -72,6 +74,7 @@ export const useSwapDisplayValues = ({ buyAssetId: buyAsset.assetId, sellAmountCryptoBaseUnit: sellAmountBaseUnit, allowedSwapperNames, + refetchInterval: ratesRefetchInterval, enabled: !!sellAmountBaseUnit && sellAmountBaseUnit !== '0' && diff --git a/packages/swapper/package.json b/packages/swapper/package.json index c2fb38a40fe..a3802d1af4f 100644 --- a/packages/swapper/package.json +++ b/packages/swapper/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/swapper", - "version": "17.7.3", + "version": "17.8.0", "repository": "https://github.com/shapeshift/web", "license": "MIT", "type": "module", diff --git a/packages/types/package.json b/packages/types/package.json index 360eb29ced4..d6dd2b1cac7 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/types", - "version": "8.6.7", + "version": "8.6.8", "description": "Common types shared across packages", "repository": "https://github.com/shapeshift/web", "license": "MIT", diff --git a/packages/unchained-client/package.json b/packages/unchained-client/package.json index b6d20a3930a..5d07f18a1e3 100644 --- a/packages/unchained-client/package.json +++ b/packages/unchained-client/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/unchained-client", - "version": "10.14.10", + "version": "10.14.11", "repository": "https://github.com/shapeshift/web", "license": "MIT", "type": "module", diff --git a/packages/utils/package.json b/packages/utils/package.json index 45115752c37..c471d627d87 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@shapeshiftoss/utils", - "version": "1.0.5", + "version": "1.1.0", "repository": "https://github.com/shapeshift/web", "license": "MIT", "type": "module", diff --git a/src/Routes/RoutesCommon.tsx b/src/Routes/RoutesCommon.tsx index 9fffb7aa82a..20e9854772b 100644 --- a/src/Routes/RoutesCommon.tsx +++ b/src/Routes/RoutesCommon.tsx @@ -48,6 +48,16 @@ const Dashboard = makeSuspenseful( true, ) +const Foxy = makeSuspenseful( + lazy(() => + import('@/pages/Foxy/Foxy').then(({ Foxy }) => ({ + default: Foxy, + })), + ), + {}, + true, +) + const Asset = makeSuspenseful( lazy(() => import('@/pages/Assets/Asset').then(({ Asset }) => ({ @@ -420,6 +430,11 @@ export const routes: Route[] = [ main: WalletConnectDeepLink, hide: true, }, + { + path: '/foxy', + main: Foxy, + hide: true, + }, { path: '/limit/*', label: '', diff --git a/src/assets/translations/de/main.json b/src/assets/translations/de/main.json index a07a48053f8..8bbbbe1f817 100644 --- a/src/assets/translations/de/main.json +++ b/src/assets/translations/de/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Ausstehende Stake Beendigungs-Anfragen werden hier angezeigt.", "availableDate": "verfügbar %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Abhebungen", "emptyWithdraws": "Ausstehende oder aktive Auszahlungen werden hier angezeigt.", - "availableDate": "Verfügbar %{date}", - "disabledTitle": "FOXy Einzahlungen sind deaktiviert" - }, - "foxFarmingOverview": { "header": "Zahlen Sie ETH-FOX LP Tokens bei %{opportunity} ein, um zu verdienen", "body": "Genehmigen und Staken Sie Ihre Liquiditätstoken, um Ihre FOX Bonusprämien zu erhalten.", "rewards": "Die Belohnungen, die Sie verdienen, fallen automatisch an.", @@ -857,12 +853,6 @@ "swap": "Handel", "swapRefund": "Swap Rückerstattung" }, - "foxy": { - "stake": "Stake", - "unstake": "Staking beenden", - "instantUnstake": "Staking sofort beenden", - "claimWithdraw": "Abhebung anfordern" - }, "erc20": { "approve": "Genehmigen", "approveSymbol": "Genehmige %{symbol}", diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 5f01cfdd09a..f0766ee6162 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Pending unstaking requests will show here.", "availableDate": "available %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Withdrawals", "emptyWithdraws": "Pending or active withdrawals will show here.", - "availableDate": "available %{date}", - "disabledTitle": "FOXy deposits are disabled" - }, - "foxFarmingOverview": { "header": "Deposit ETH-FOX LP tokens to %{opportunity} to earn", "body": "Approve and stake your liquidity tokens to earn bonus FOX rewards.", "rewards": "The rewards you earn will accrue automatically.", @@ -857,12 +853,6 @@ "swap": "Trade", "swapRefund": "Swap Refund" }, - "foxy": { - "stake": "Stake", - "unstake": "Unstake", - "instantUnstake": "Instant Unstake", - "claimWithdraw": "Claim Withdraw" - }, "erc20": { "approve": "Approve", "approveSymbol": "Approve %{symbol}", diff --git a/src/assets/translations/es/main.json b/src/assets/translations/es/main.json index 6102b148649..0a0a9960ea8 100644 --- a/src/assets/translations/es/main.json +++ b/src/assets/translations/es/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Aquí se mostrarán las solicitudes pendientes de retirar Staking.", "availableDate": "disponible %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Retiros", "emptyWithdraws": "Los retiros pendientes o activos se mostrarán aquí.", - "availableDate": "disponible %{date}", - "disabledTitle": "Depósitos FOXy inhabilitados" - }, - "foxFarmingOverview": { "header": "Deposite tokens LP ETH-FOX en %{opportunity} para ganar", "body": "Aprueba y Stake tus tokens de liquidez para ganar recompensas adicionales en FOX.", "rewards": "Las recompensas se acumulan automáticamente.", @@ -857,12 +853,6 @@ "swap": "Intercambio", "swapRefund": "Reembolso de Intercambio" }, - "foxy": { - "stake": "Stake", - "unstake": "Retirar de Stake", - "instantUnstake": "Retirar de Stake al instante", - "claimWithdraw": "Retirar monto a reclamar" - }, "erc20": { "approve": "Aprobar", "approveSymbol": "Aprobar %{symbol}", diff --git a/src/assets/translations/fr/main.json b/src/assets/translations/fr/main.json index 355e4c10e9c..970065142e1 100644 --- a/src/assets/translations/fr/main.json +++ b/src/assets/translations/fr/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Les demandes de déstaking en attente s'affichent ici.", "availableDate": "disponible %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Retraits", "emptyWithdraws": "Les retraits en attente ou actifs s'afficheront ici.", - "availableDate": "disponible %{date}", - "disabledTitle": "Les dépôts FOXy sont désactivés" - }, - "foxFarmingOverview": { "header": "Déposer des jetons LP ETH-FOX dans %{opportunity} pour gagner", "body": "Autorisez et stakez vos jetons de liquidité pour gagner des récompenses bonus en FOX.", "rewards": "Les récompenses que vous gagnez s'accumuleront automatiquement.", @@ -857,12 +853,6 @@ "swap": "Échange", "swapRefund": "Remboursement d'échange" }, - "foxy": { - "stake": "Stake", - "unstake": "Déstake", - "instantUnstake": "Déstake instantané", - "claimWithdraw": "Réclamation de retrait" - }, "erc20": { "approve": "Autorisation", "approveSymbol": "Autorisation %{symbol}", diff --git a/src/assets/translations/glossary.json b/src/assets/translations/glossary.json index d79b5e2908c..2bb2d12b085 100644 --- a/src/assets/translations/glossary.json +++ b/src/assets/translations/glossary.json @@ -59,7 +59,6 @@ "CowSwap": null, "Jupiter": null, "Chainflip": null, - "FOXy": null, "stake": { "de": "Stake", "es": "stake", diff --git a/src/assets/translations/id/main.json b/src/assets/translations/id/main.json index f7c2e53c306..aebec99dc56 100644 --- a/src/assets/translations/id/main.json +++ b/src/assets/translations/id/main.json @@ -143,16 +143,6 @@ "learnMore": "Pelajari Lebih Lanjut " } }, - "foxyOverview": { - "header": "Setor FOX untuk mendapatkan FOXy untuk menghasilkan", - "body": "FOXy adalah token rebasing yang dipatok dengan harga FOX. Staked FOX bekerja di berbagai protokol DeFi untuk mendapatkan hasil terbaik.", - "rewards": "Imbalan yang Anda peroleh akan bertambah secara otomatis.", - "cta": "Setor FOX Sekarang ", - "foxyBalance": "Saldo FOXy", - "withdrawals": "Penarikan", - "emptyWithdraws": "Penarikan tertunda atau aktif akan ditampilkan di sini.", - "availableDate": "tersedia %{date}" - }, "claim": { "rewardDepositInfo": "Hadiah Anda akan disimpan ke dompet utama Anda.", "title": "Klaim Hadiah Staking", @@ -301,12 +291,6 @@ }, "thor": { "transferOut": "Transfer keluar" - }, - "foxy": { - "stake": "Stake", - "unstake": "Unstake", - "instantUnstake": "Unstake Instan", - "claimWithdraw": "Klaim Penarikan" } }, "unknown": "Transaksi" diff --git a/src/assets/translations/ja/main.json b/src/assets/translations/ja/main.json index f9af44b10bc..2279ce95d76 100644 --- a/src/assets/translations/ja/main.json +++ b/src/assets/translations/ja/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "保留中のステーキング解除のリクエストはここに表示されます。", "availableDate": "利用可能 %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "出金", "emptyWithdraws": "保留中の出金またはアクティブな出金はここに表示されます。", - "availableDate": "利用可能 %{date}", - "disabledTitle": "FOXyデポジットは無効になっています" - }, - "foxFarmingOverview": { "header": "ETH-FOX LP トークンを %{opportunity} に入金して獲得する", "body": "流動性トークンを承認してステーキングして、ボーナスFOX報酬を獲得する", "rewards": "獲得した報酬は自動的に蓄積されます。", @@ -857,12 +853,6 @@ "swap": "トレード", "swapRefund": "スワップ払い戻し" }, - "foxy": { - "stake": "ステーク", - "unstake": "ステークを解除する", - "instantUnstake": "一時的にステークを解除する", - "claimWithdraw": "出金の獲得" - }, "erc20": { "approve": "承認する", "approveSymbol": "%{symbol}を承認", diff --git a/src/assets/translations/ko/main.json b/src/assets/translations/ko/main.json index 43ef30f4be6..6735ca56cf5 100644 --- a/src/assets/translations/ko/main.json +++ b/src/assets/translations/ko/main.json @@ -144,16 +144,6 @@ "learnMore": "더 알아보기" } }, - "foxyOverview": { - "header": "FOXy를 적립하려면 FOX를 입금하세요.", - "body": "FOXy는 FOX 가격에 페깅되어 있는 리베이스 토큰입니다. 스테이크된 FOX는 다양한 DeFi 프로토콜에 연동되어 가능한 한 최고의 수익을 얻을 수 있게 합니다.", - "rewards": "획득한 보상은 자동으로 적립됩니다.", - "cta": "FOX 입금하기", - "foxyBalance": "FOXy 밸런스", - "withdrawals": "출금", - "emptyWithdraws": "보류 중이거나 활성인 출금이 여기에 표시됩니다.", - "availableDate": "가능한 날짜 %{date}" - }, "claim": { "rewardDepositInfo": "보상은 기본 지갑에 입금됩니다.", "title": "스테이킹 보상 청구", @@ -298,12 +288,6 @@ "thor": { "transferOut": "출금" }, - "foxy": { - "stake": "스테이크", - "unstake": "언스테이크", - "instantUnstake": "즉시 언스테이크", - "claimWithdraw": "출금 요청" - }, "erc20": { "approve": "승인" } @@ -1028,12 +1012,10 @@ "tradingUnavailable": "아직 ShapeShift에서는 %{assetSymbol} 거래가 불가능합니다. 대신 Elastic Swap에서 %{assetSymbol} 거래가 가능합니다.", "tradeOnElasticSwap": "ElasticSwap에서 %{assetSymbol} 거래하기", "otherOpportunitiesTitle": { - "FOX": "다른 수익 기회", - "FOXy": "수익 기회" + "FOX": "다른 수익 기회" }, "otherOpportunitiesDescription": { - "FOX": "유동성 공급, 이자 농사, 대출 및 차입 등의 다양한 기회를 확인하세요. 아래의 시작하기 링크를 클릭하여 관심있는 외부 사이트를 방문하여 자세히 알아보시고, FOX를 사용해보세요.", - "FOXy": "이자 농사와 비용 수익을 제공하는, 업계 첫 DAO 리베이스 토큰인 FOXy를 빠르고 쉽게 구매해보세요!" + "FOX": "유동성 공급, 이자 농사, 대출 및 차입 등의 다양한 기회를 확인하세요. 아래의 시작하기 링크를 클릭하여 관심있는 외부 사이트를 방문하여 자세히 알아보시고, FOX를 사용해보세요." }, "liquidityPools": "유동성 풀", "farming": "이자 농사", diff --git a/src/assets/translations/pt/main.json b/src/assets/translations/pt/main.json index 1216252ee2f..fc4562763c5 100644 --- a/src/assets/translations/pt/main.json +++ b/src/assets/translations/pt/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "As solicitações de retirada pendentes serão exibidas aqui.", "availableDate": "disponível em %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Saques", "emptyWithdraws": "Saques pendentes ou em processo de pagamento serão exibidos aqui.", - "availableDate": "Disponível %{date}", - "disabledTitle": "Depósitos de FOXy estão desabilitados" - }, - "foxFarmingOverview": { "header": "Deposite tokens LP de ETH-FOX em %{opportunity} para ganhar", "body": "Aprove e faça stake de seus tokens de liquidez para ganhar recompensas FOX.", "rewards": "As recompensas que você ganhar serão acumuladas automaticamente.", @@ -857,12 +853,6 @@ "swap": "Negocie", "swapRefund": "Reembolso Swap" }, - "foxy": { - "stake": "Stake", - "unstake": "Remover Staking", - "instantUnstake": "Stake instantâneo ", - "claimWithdraw": "Reivindicar Retirada" - }, "erc20": { "approve": "Aprovar", "approveSymbol": "Aprovar %{symbol}", diff --git a/src/assets/translations/ru/main.json b/src/assets/translations/ru/main.json index 5b4f37946f2..0ead9c68aba 100644 --- a/src/assets/translations/ru/main.json +++ b/src/assets/translations/ru/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Здесь отображаются незавершенные запросы на снятие стейкинга.", "availableDate": "доступно %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Вывод средств", "emptyWithdraws": "Здесь отображаются ожидающие или активные выводы средств.", - "availableDate": "доступен %{date}", - "disabledTitle": "Депозиты FOXy не работают" - }, - "foxFarmingOverview": { "header": "Внесите токены ETH-FOX LP на %{opportunity}, чтобы заработать", "body": "Подтвердите и стейкайте свои токены ликвидности, чтобы получить бонусные вознаграждения FOX.", "rewards": "Вознаграждения, которые вы заработаете, будут начисляться автоматически.", @@ -857,12 +853,6 @@ "swap": "Торговля", "swapRefund": "Возврат обмена" }, - "foxy": { - "stake": "Стейк", - "unstake": "Разстейкать", - "instantUnstake": "Мгновенная отмена стейкинга", - "claimWithdraw": "Подтвердить вывод" - }, "erc20": { "approve": "Подтвердить", "approveSymbol": "Утвердить %{symbol}", diff --git a/src/assets/translations/tr/main.json b/src/assets/translations/tr/main.json index 1f86248af18..2da15f2d398 100644 --- a/src/assets/translations/tr/main.json +++ b/src/assets/translations/tr/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Bekleyen staking istekleri burada gösterilir.", "availableDate": "kullanılabilir %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Para Çekme", "emptyWithdraws": "Bekleyen veya etkin para çekme işlemleri burada gösterilir.", - "availableDate": "kullanılabilir %{date}", - "disabledTitle": "FOXy para yatırma işlemleri devre dışı bırakıldı" - }, - "foxFarmingOverview": { "header": "Kazanmak için ETH-FOX LP jetonlarını %{opportunity}'e yatırın", "body": "Bonus FOX ödülleri kazanmak için likidite jetonlarınızı onaylayın ve stake edin.", "rewards": "Kazandığınız ödüller otomatik olarak tahakkuk edecektir.", @@ -857,12 +853,6 @@ "swap": "Takas", "swapRefund": "Takas İadesi" }, - "foxy": { - "stake": "Stake Et", - "unstake": "Stake Kaldır", - "instantUnstake": "Stake'i Anında Geri Al", - "claimWithdraw": "Ödülleri Topla" - }, "erc20": { "approve": "Onayla", "approveSymbol": "%{symbol} Onayla", diff --git a/src/assets/translations/uk/main.json b/src/assets/translations/uk/main.json index b321bb806ba..1fe8227ace1 100644 --- a/src/assets/translations/uk/main.json +++ b/src/assets/translations/uk/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "Тут відображатимуться запити, що очікують на розблокування.", "availableDate": "доступно %{date}" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "Виведення", "emptyWithdraws": "Тут відображаються очікувані або активні зняття коштів.", - "availableDate": "доступно %{date}", - "disabledTitle": "Депозити FOXy відключені" - }, - "foxFarmingOverview": { "header": "Внесіть LP токени ETH-FOX у %{opportunity}, щоб заробити", "body": "Схвалюйте та стейкайте свої токени ліквідності, щоб отримати бонусні винагороди FOX.", "rewards": "Зароблені вами винагороди будуть нараховуватися автоматично.", @@ -857,12 +853,6 @@ "swap": "Торгівля", "swapRefund": "Повернення коштів за обмін" }, - "foxy": { - "stake": "Стейк", - "unstake": "Зняти зі стейкінгу", - "instantUnstake": "Миттєве розблокування", - "claimWithdraw": "Підтвердити зняття" - }, "erc20": { "approve": "Схвалити", "approveSymbol": "Схвалити %{symbol}", diff --git a/src/assets/translations/zh/main.json b/src/assets/translations/zh/main.json index 2371d9dee79..50618da93cc 100644 --- a/src/assets/translations/zh/main.json +++ b/src/assets/translations/zh/main.json @@ -581,13 +581,9 @@ "emptyWithdraws": "此处将显示待处理的取消质押请求。", "availableDate": "在 %{date} 可用" }, - "foxyOverview": { + "foxFarmingOverview": { "withdrawals": "提现", "emptyWithdraws": "此处将显示待处理或进行的提现。", - "availableDate": "在 %{date} 可用", - "disabledTitle": "FOXy 充值已被禁用" - }, - "foxFarmingOverview": { "header": "充值 ETH-FOX LP 代币 %{opportunity} 来赚取收益", "body": "授权并质押你的流动性代币以获得额外的 FOX 奖励。", "rewards": "你获得的奖励将自动累积。", @@ -857,12 +853,6 @@ "swap": "交易", "swapRefund": "交换退款" }, - "foxy": { - "stake": "质押", - "unstake": "取消质押", - "instantUnstake": "立即取消质押", - "claimWithdraw": "领取提款" - }, "erc20": { "approve": "授权", "approveSymbol": "授权 %{symbol}", diff --git a/src/components/StakingVaults/EarnOpportunities.tsx b/src/components/StakingVaults/EarnOpportunities.tsx index 2c02767906b..068cfbfd3b9 100644 --- a/src/components/StakingVaults/EarnOpportunities.tsx +++ b/src/components/StakingVaults/EarnOpportunities.tsx @@ -1,6 +1,6 @@ import { Box, Card, CardBody, CardHeader, Heading, HStack } from '@chakra-ui/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' -import { foxAssetId, foxyAssetId, fromAssetId } from '@shapeshiftoss/caip' +import { fromAssetId } from '@shapeshiftoss/caip' import qs from 'qs' import { useCallback, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' @@ -56,13 +56,13 @@ export const EarnOpportunitiesContent = ({ assetId, accountId }: EarnOpportuniti () => !asset ? [] - : lpOpportunities.concat(stakingOpportunities).filter( - row => - row.assetId.toLowerCase() === asset.assetId.toLowerCase() || - (row.underlyingAssetIds.length && row.underlyingAssetIds.includes(asset.assetId)) || - // show foxy opportunity in the foxy asset page - (row.assetId === foxAssetId && asset.assetId === foxyAssetId), - ), + : lpOpportunities + .concat(stakingOpportunities) + .filter( + row => + row.assetId.toLowerCase() === asset.assetId.toLowerCase() || + (row.underlyingAssetIds.length && row.underlyingAssetIds.includes(asset.assetId)), + ), [asset, lpOpportunities, stakingOpportunities], ) diff --git a/src/features/defi/contexts/DefiManagerProvider/utils.ts b/src/features/defi/contexts/DefiManagerProvider/utils.ts index 4d66759c657..d8107bf7048 100644 --- a/src/features/defi/contexts/DefiManagerProvider/utils.ts +++ b/src/features/defi/contexts/DefiManagerProvider/utils.ts @@ -1,6 +1,5 @@ import { CosmosManager } from '@/features/defi/providers/cosmos/components/CosmosManager/CosmosManager' import { FoxFarmingManager } from '@/features/defi/providers/fox-farming/components/FoxFarmingManager/FoxFarmingManager' -import { FoxyManager } from '@/features/defi/providers/foxy/components/FoxyManager/FoxyManager' import { ThorchainSaversManager } from '@/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/ThorchainSaversManager' import { DefiProvider, DefiType } from '@/state/slices/opportunitiesSlice/types' @@ -14,7 +13,6 @@ export const DefiProviderToDefiModuleResolverByDeFiType = { [DefiProvider.ThorchainSavers]: { [`${DefiType.Staking}`]: ThorchainSaversManager, }, - [DefiProvider.ShapeShift]: FoxyManager, [DefiProvider.CosmosSdk]: CosmosManager, } // Not curried since we can either have a list of providers by DefiType, or a single one for providers not yet migrated to the abstraction diff --git a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Overview/WithdrawCard.tsx b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Overview/WithdrawCard.tsx index 78606d1aed5..645ee87769e 100644 --- a/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Overview/WithdrawCard.tsx +++ b/src/features/defi/providers/fox-farming/components/FoxFarmingManager/Overview/WithdrawCard.tsx @@ -58,9 +58,9 @@ export const WithdrawCard = ({ asset, amount, expired }: WithdrawCardProps) => { return ( - + {!hasClaim ? ( - + ) : ( - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/FoxyManager.tsx b/src/features/defi/providers/foxy/components/FoxyManager/FoxyManager.tsx deleted file mode 100644 index 2ff1f6a3cf5..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/FoxyManager.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { AccountId } from '@shapeshiftoss/caip' -import { AnimatePresence } from 'framer-motion' -import { useState } from 'react' - -import { FoxyDeposit } from './Deposit/FoxyDeposit' -import { FoxyClaim } from './Overview/Claim/Claim' -import { FoxyOverview } from './Overview/FoxyOverview' -import { FoxyWithdraw } from './Withdraw/FoxyWithdraw' - -import { SlideTransition } from '@/components/SlideTransition' -import type { - DefiParams, - DefiQueryParams, -} from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { DefiAction } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' - -export const FoxyManager = () => { - const { query } = useBrowserRouter() - const { modal } = query - const [accountId, setAccountId] = useState() - - return ( - - {modal === DefiAction.Overview && ( - - - - )} - {modal === DefiAction.Deposit && ( - - - - )} - {modal === DefiAction.Withdraw && ( - - - - )} - {modal === DefiAction.Claim && ( - - - - )} - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/Claim.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/Claim.tsx deleted file mode 100644 index 09546f69e11..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/Claim.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { AccountId } from '@shapeshiftoss/caip' -import qs from 'qs' -import { useCallback } from 'react' -import { useTranslate } from 'react-polyglot' -import { MemoryRouter, useNavigate } from 'react-router-dom' - -import { ClaimRoutes } from './ClaimRoutes' - -import { SlideTransition } from '@/components/SlideTransition' -import { DefiModalHeader } from '@/features/defi/components/DefiModal/DefiModalHeader' -import type { - DefiParams, - DefiQueryParams, -} from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { DefiAction } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' - -export const FoxyClaim: React.FC<{ - accountId: AccountId | undefined -}> = ({ accountId }) => { - const translate = useTranslate() - const { query, location } = useBrowserRouter() - const navigate = useNavigate() - - const handleBack = useCallback(() => { - navigate({ - pathname: location.pathname, - search: qs.stringify({ - ...query, - modal: DefiAction.Overview, - }), - }) - }, [navigate, location.pathname, query]) - - return ( - - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx deleted file mode 100644 index 61020fefb9c..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimConfirm.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import { - Button, - Link, - ModalBody, - ModalFooter, - Skeleton, - SkeletonText, - Stack, - useToast, -} from '@chakra-ui/react' -import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' -import { ASSET_NAMESPACE, ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' -import { supportsETH } from '@shapeshiftoss/hdwallet-core/wallet' -import { KnownChainIds } from '@shapeshiftoss/types' -import { BigAmount } from '@shapeshiftoss/utils' -import dayjs from 'dayjs' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslate } from 'react-polyglot' -import { useNavigate } from 'react-router-dom' - -import { Amount } from '@/components/Amount/Amount' -import { AssetIcon } from '@/components/AssetIcon' -import { InlineCopyButton } from '@/components/InlineCopyButton' -import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis' -import { Row } from '@/components/Row/Row' -import { SlideTransition } from '@/components/SlideTransition' -import { Text } from '@/components/Text' -import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton' -import { useWallet } from '@/hooks/useWallet/useWallet' -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import type { StakingId } from '@/state/slices/opportunitiesSlice/types' -import { - serializeUserStakingId, - supportsUndelegations, -} from '@/state/slices/opportunitiesSlice/utils' -import { - selectAssetById, - selectBip44ParamsByAccountId, - selectEarnUserStakingOpportunityByUserStakingId, - selectMarketDataByAssetIdUserCurrency, -} from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -type ClaimConfirmProps = { - accountId: AccountId | undefined - stakingAssetId: AssetId - amount?: string - contractAddress: string - chainId: ChainId - onBack: () => void -} - -export const ClaimConfirm = ({ - accountId, - stakingAssetId, - amount, - contractAddress, - chainId, - onBack, -}: ClaimConfirmProps) => { - const [userAddress, setUserAddress] = useState('') - const [estimatedGas, setEstimatedGas] = useState('0') - const [loading, setLoading] = useState(false) - const foxyApi = getFoxyApi() - const { state: walletState } = useWallet() - const translate = useTranslate() - const claimAmount = bnOrZero(amount).toString() - const navigate = useNavigate() - - const chainAdapterManager = getChainAdapterManager() - - // Asset Info - const stakingAsset = useAppSelector(state => selectAssetById(state, stakingAssetId)) - const assetMarketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, stakingAssetId), - ) - const feeAssetId = toAssetId({ - chainId, - assetNamespace: 'slip44', - assetReference: ASSET_REFERENCE.Ethereum, - }) - const feeAsset = useAppSelector(state => selectAssetById(state, feeAssetId)) - const feeMarketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, feeAssetId), - ) - - if (!stakingAsset) throw new Error(`Asset not found for AssetId ${stakingAssetId}`) - if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${feeAssetId}`) - - const toast = useToast() - - const accountFilter = useMemo(() => ({ accountId: accountId ?? '' }), [accountId]) - const bip44Params = useAppSelector(state => selectBip44ParamsByAccountId(state, accountFilter)) - - const cryptoPrecisionBalance = useMemo( - () => - bnOrZero( - BigAmount.fromBaseUnit({ - value: claimAmount ?? '0', - precision: stakingAsset.precision, - }).toPrecision(), - ), - [stakingAsset.precision, claimAmount], - ) - // The highest level AssetId/OpportunityId, in this case of the single FOXy contract - const assetId = toAssetId({ - chainId, - assetNamespace: ASSET_NAMESPACE.erc20, - assetReference: contractAddress, - }) - const opportunityDataFilter = useMemo(() => { - if (!accountId) return undefined - return { - userStakingId: serializeUserStakingId(accountId, assetId as StakingId), - } - }, [accountId, assetId]) - - const foxyEarnOpportunityData = useAppSelector(state => - opportunityDataFilter - ? selectEarnUserStakingOpportunityByUserStakingId(state, opportunityDataFilter) - : undefined, - ) - - const undelegations = useMemo( - () => - foxyEarnOpportunityData && supportsUndelegations(foxyEarnOpportunityData) - ? foxyEarnOpportunityData.undelegations - : undefined, - [foxyEarnOpportunityData], - ) - - const hasPendingUndelegation = Boolean( - undelegations && - undelegations.some(undelegation => - dayjs().isAfter(dayjs(undelegation.completionTime).unix()), - ), - ) - - const handleConfirm = useCallback(async () => { - if (!(walletState.wallet && contractAddress && userAddress && foxyApi && bip44Params)) return - setLoading(true) - try { - if (!supportsETH(walletState.wallet)) - throw new Error(`handleConfirm: wallet does not support ethereum`) - const txid = await foxyApi.claimWithdraw({ - claimAddress: userAddress, - userAddress, - wallet: walletState.wallet, - contractAddress, - bip44Params, - }) - navigate('/status', { - state: { - txid, - assetId: stakingAssetId, - amount, - userAddress, - estimatedGas, - chainId, - }, - }) - } catch (error) { - console.error(error) - toast({ - position: 'top-right', - description: translate('common.transactionFailedBody'), - title: translate('common.transactionFailed'), - status: 'error', - }) - } finally { - setLoading(false) - } - }, [ - amount, - stakingAssetId, - bip44Params, - chainId, - contractAddress, - estimatedGas, - foxyApi, - navigate, - toast, - translate, - userAddress, - walletState?.wallet, - ]) - - useEffect(() => { - if (!bip44Params) return - ;(async () => { - try { - const chainAdapter = await chainAdapterManager.get(KnownChainIds.EthereumMainnet) - if (!(walletState.wallet && contractAddress && foxyApi && chainAdapter)) return - if (!supportsETH(walletState.wallet)) - throw new Error(`ClaimConfirm::useEffect: wallet does not support ethereum`) - - const { accountNumber } = bip44Params - const userAddress = await chainAdapter.getAddress({ - wallet: walletState.wallet, - accountNumber, - }) - setUserAddress(userAddress) - const feeDataEstimate = await foxyApi.estimateClaimWithdrawFees({ - claimAddress: userAddress, - userAddress, - contractAddress, - wallet: walletState.wallet, - bip44Params, - }) - - const { - chainSpecific: { gasPrice, gasLimit }, - } = feeDataEstimate.fast - - const gasEstimate = bnOrZero(gasPrice).times(gasLimit).toFixed(0) - setEstimatedGas(gasEstimate) - } catch (error) { - // TODO: handle client side errors - console.error(error) - } - })() - }, [ - bip44Params, - chainAdapterManager, - contractAddress, - feeAsset.precision, - feeMarketData?.price, - foxyApi, - walletState.wallet, - ]) - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimRoutes.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimRoutes.tsx deleted file mode 100644 index a46a826b67b..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimRoutes.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { AccountId } from '@shapeshiftoss/caip' -import { AnimatePresence } from 'framer-motion' -import { useMemo } from 'react' -import { Route, Routes } from 'react-router-dom' - -import { ClaimConfirm } from './ClaimConfirm' -import { ClaimStatus } from './ClaimStatus' - -import { SlideTransition } from '@/components/SlideTransition' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { - makeTotalUndelegationsCryptoBaseUnit, - serializeUserStakingId, - supportsUndelegations, - toOpportunityId, -} from '@/state/slices/opportunitiesSlice/utils' -import { selectEarnUserStakingOpportunityByUserStakingId } from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -type ClaimRouteProps = { - accountId: AccountId | undefined - onBack: () => void -} - -export const ClaimRoutes: React.FC = ({ onBack, accountId }) => { - const { contractAddress, stakingAssetId, chainId } = useFoxyQuery() - - const opportunityDataFilter = useMemo(() => { - return { - userStakingId: serializeUserStakingId( - accountId ?? '', - toOpportunityId({ - chainId, - assetNamespace: 'erc20', - assetReference: contractAddress, - }), - ), - } - }, [accountId, chainId, contractAddress]) - - const foxyEarnOpportunityData = useAppSelector(state => - opportunityDataFilter - ? selectEarnUserStakingOpportunityByUserStakingId(state, opportunityDataFilter) - : undefined, - ) - - const undelegationAmount = useMemo( - () => - foxyEarnOpportunityData && supportsUndelegations(foxyEarnOpportunityData) - ? makeTotalUndelegationsCryptoBaseUnit(foxyEarnOpportunityData.undelegations).toFixed() - : '0', - [foxyEarnOpportunityData], - ) - - const claimConfirmElement = useMemo( - () => ( - - ), - [stakingAssetId, accountId, chainId, contractAddress, onBack, undelegationAmount], - ) - - const claimStatusElement = useMemo(() => , [accountId]) - - return ( - - - - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx deleted file mode 100644 index 05bde1622e9..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/Claim/ClaimStatus.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import { Box, Button, Center, Link, ModalBody, ModalFooter, Stack } from '@chakra-ui/react' -import type { AccountId } from '@shapeshiftoss/caip' -import { ASSET_REFERENCE, toAssetId } from '@shapeshiftoss/caip' -import { TxStatus } from '@shapeshiftoss/unchained-client' -import { BigAmount } from '@shapeshiftoss/utils' -import type { TransactionReceipt, TransactionReceiptParams } from 'ethers' -import isNil from 'lodash/isNil' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { FaCheck, FaTimes } from 'react-icons/fa' -import { useTranslate } from 'react-polyglot' -import { useLocation } from 'react-router-dom' - -import { Amount } from '@/components/Amount/Amount' -import { AssetIcon } from '@/components/AssetIcon' -import { CircularProgress } from '@/components/CircularProgress/CircularProgress' -import { IconCircle } from '@/components/IconCircle' -import { InlineCopyButton } from '@/components/InlineCopyButton' -import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis' -import { Row } from '@/components/Row/Row' -import { SlideTransition } from '@/components/SlideTransition' -import { RawText } from '@/components/Text' -import { useSafeTxQuery } from '@/hooks/queries/useSafeTx' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' -import { usePoll } from '@/hooks/usePoll/usePoll' -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { opportunitiesApi } from '@/state/slices/opportunitiesSlice/opportunitiesApiSlice' -import { DefiProvider, DefiType } from '@/state/slices/opportunitiesSlice/types' -import { selectAssetById, selectMarketDataByAssetIdUserCurrency } from '@/state/slices/selectors' -import { useAppDispatch, useAppSelector } from '@/state/store' - -type ClaimState = { - txStatus: TxStatus - usedGasFeeCryptoBaseUnit?: string -} - -const StatusInfo = { - [TxStatus.Pending]: { - text: 'defi.broadcastingTransaction', - color: 'blue.500', - icon: undefined, - }, - [TxStatus.Unknown]: { - text: 'defi.transactionUnknown', - color: 'gray.500', - icon: undefined, - }, - [TxStatus.Confirmed]: { - text: 'defi.transactionComplete', - color: 'green.500', - icon: , - }, - [TxStatus.Failed]: { - text: 'defi.transactionFailed', - color: 'red.500', - icon: , - }, -} - -type ClaimStatusProps = { - accountId: AccountId | undefined -} - -export const ClaimStatus: React.FC = ({ accountId }) => { - const { poll } = usePoll() - const { navigate } = useBrowserRouter() - const foxyApi = getFoxyApi() - const translate = useTranslate() - const { - state: { txid, amount, assetId, userAddress, estimatedGas, chainId }, - } = useLocation() - const [state, setState] = useState({ - txStatus: TxStatus.Pending, - }) - - const { data: maybeSafeTx } = useSafeTxQuery({ - maybeSafeTxHash: txid, - accountId, - }) - - // Asset Info - const asset = useAppSelector(state => selectAssetById(state, assetId)) - if (!asset) throw new Error(`Asset not found for AssetId ${assetId}`) - - const feeAssetId = toAssetId({ - chainId, - assetNamespace: 'slip44', - assetReference: ASSET_REFERENCE.Ethereum, - }) - const feeAsset = useAppSelector(state => selectAssetById(state, feeAssetId)) - if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${feeAssetId}`) - - const feeMarketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, feeAssetId), - ) - - const dispatch = useAppDispatch() - // TODO: maybeRefetchOpportunities heuristics - const refetchFoxyBalances = useCallback(() => { - if (!accountId) return - - dispatch( - opportunitiesApi.endpoints.getOpportunitiesUserData.initiate( - [ - { - accountId, - defiType: DefiType.Staking, - defiProvider: DefiProvider.ShapeShift, - }, - ], - { forceRefetch: true }, - ), - ) - }, [accountId, dispatch]) - - useEffect(() => { - ;(async () => { - if (!foxyApi || !txid) return - try { - const transactionReceipt = await poll({ - fn: () => foxyApi.getTxReceipt({ txid }), - validate: (result: TransactionReceipt | null) => !isNil(result), - interval: 15000, - maxAttempts: 30, - }) - - if (transactionReceipt?.status) { - refetchFoxyBalances() - } - - if (maybeSafeTx?.isQueuedSafeTx) { - return setState({ - ...state, - txStatus: TxStatus.Pending, - }) - } - - if (maybeSafeTx?.isExecutedSafeTx) { - return setState({ - ...state, - txStatus: TxStatus.Confirmed, - usedGasFeeCryptoBaseUnit: bnOrZero(maybeSafeTx.transaction?.gasUsed).toString(), - }) - } - - setState({ - ...state, - txStatus: transactionReceipt?.status ? TxStatus.Confirmed : TxStatus.Failed, - usedGasFeeCryptoBaseUnit: bnOrZero( - (transactionReceipt as TransactionReceiptParams | null)?.effectiveGasPrice?.toString(), - ) - .times(bnOrZero(transactionReceipt?.gasUsed.toString())) - .toString(), - }) - } catch (error) { - console.error(error) - setState({ - ...state, - txStatus: TxStatus.Failed, - usedGasFeeCryptoBaseUnit: estimatedGas, - }) - } - })() - }, [ - refetchFoxyBalances, - estimatedGas, - foxyApi, - state, - txid, - poll, - maybeSafeTx?.transaction?.transactionHash, - maybeSafeTx?.transaction?.gasUsed, - maybeSafeTx?.isQueuedSafeTx, - maybeSafeTx?.isExecutedSafeTx, - ]) - - const handleClose = useMemo(() => () => navigate(-1), [navigate]) - - return ( - - -
- - - {state.txStatus === TxStatus.Pending ? ( - - ) : ( - - {StatusInfo[state.txStatus].icon} - - )} - - - - {translate( - state.txStatus === TxStatus.Pending - ? 'defi.broadcastingTransaction' - : 'defi.transactionComplete', - )} - -
-
- - - - {translate('modals.status.transactionId')} - - - - - - - - {translate('defi.modals.claim.claimAmount')} - - - - - - {translate('defi.modals.claim.claimToAddress')} - - - - - - - - - - - {translate( - state.txStatus === TxStatus.Pending - ? 'modals.status.estimatedGas' - : 'modals.status.gasUsed', - )} - - - - - - - - - - - -
- ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx deleted file mode 100644 index 98d0bf44c7d..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/FoxyOverview.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { ArrowDownIcon, ArrowUpIcon } from '@chakra-ui/icons' -import { Center } from '@chakra-ui/react' -import type { AccountId } from '@shapeshiftoss/caip' -import { ASSET_NAMESPACE, fromAccountId, toAssetId } from '@shapeshiftoss/caip' -import { BigAmount } from '@shapeshiftoss/utils' -import dayjs from 'dayjs' -import { useEffect, useMemo, useState } from 'react' -import { FaGift } from 'react-icons/fa' -import { useTranslate } from 'react-polyglot' - -import { WithdrawCard } from './WithdrawCard' - -import type { AccountDropdownProps } from '@/components/AccountDropdown/AccountDropdown' -import { CircularProgress } from '@/components/CircularProgress/CircularProgress' -import { DefiModalContent } from '@/features/defi/components/DefiModal/DefiModalContent' -import { Overview } from '@/features/defi/components/Overview/Overview' -import type { - DefiParams, - DefiQueryParams, -} from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { DefiAction } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { useGetAssetDescriptionQuery } from '@/state/slices/assetsSlice/assetsSlice' -import type { StakingId } from '@/state/slices/opportunitiesSlice/types' -import { - makeDefiProviderDisplayName, - serializeUserStakingId, - supportsUndelegations, -} from '@/state/slices/opportunitiesSlice/utils' -import { preferences } from '@/state/slices/preferencesSlice/preferencesSlice' -import { - selectEarnUserStakingOpportunityByUserStakingId, - selectFirstAccountIdByChainId, - selectHighestStakingBalanceAccountIdByStakingId, - selectMarketDataByAssetIdUserCurrency, -} from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -type FoxyOverviewProps = { - accountId: AccountId | undefined - onAccountIdChange: AccountDropdownProps['onChange'] -} - -export const FoxyOverview: React.FC = ({ - accountId, - onAccountIdChange: handleAccountIdChange, -}) => { - const { query } = useBrowserRouter() - const { chainId } = query - const { - contractAddress, - stakingAsset, - underlyingAsset: rewardAsset, - stakingAssetId, - } = useFoxyQuery() - const foxyApi = getFoxyApi() - const [canClaim, setCanClaim] = useState(null) - // The highest level AssetId/OpportunityId, in this case of the single FOXy contract - const assetId = toAssetId({ - chainId, - assetNamespace: ASSET_NAMESPACE.erc20, - assetReference: contractAddress, - }) - - const highestBalanceAccountIdFilter = useMemo( - () => ({ stakingId: assetId as StakingId }), - [assetId], - ) - const highestBalanceAccountId = useAppSelector(state => - selectHighestStakingBalanceAccountIdByStakingId(state, highestBalanceAccountIdFilter), - ) - - const translate = useTranslate() - - const defaultAccountId = useAppSelector(state => selectFirstAccountIdByChainId(state, chainId)) - const maybeAccountId = accountId ?? highestBalanceAccountId ?? defaultAccountId - - useEffect(() => { - if (!maybeAccountId) return - handleAccountIdChange(maybeAccountId) - }, [handleAccountIdChange, maybeAccountId]) - - useEffect(() => { - if (!maybeAccountId) return - ;(async () => { - const canClaimWithdraw = await foxyApi.canClaimWithdraw({ - contractAddress, - userAddress: fromAccountId(maybeAccountId).account, - }) - setCanClaim(canClaimWithdraw) - })() - }, [contractAddress, foxyApi, maybeAccountId]) - - const opportunityDataFilter = useMemo(() => { - const userStakingAccountId = accountId ?? highestBalanceAccountId ?? '' - if (!userStakingAccountId) return undefined - return { - userStakingId: serializeUserStakingId(userStakingAccountId, assetId as StakingId), - } - }, [accountId, assetId, highestBalanceAccountId]) - - const foxyEarnOpportunityData = useAppSelector(state => - opportunityDataFilter - ? selectEarnUserStakingOpportunityByUserStakingId(state, opportunityDataFilter) - : undefined, - ) - - const undelegations = useMemo( - () => - foxyEarnOpportunityData && supportsUndelegations(foxyEarnOpportunityData) - ? foxyEarnOpportunityData.undelegations - : undefined, - [foxyEarnOpportunityData], - ) - - const marketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, stakingAssetId), - ) - const cryptoAmountAvailablePrecision = bnOrZero( - BigAmount.fromBaseUnit({ - value: foxyEarnOpportunityData?.stakedAmountCryptoBaseUnit ?? '0', - precision: stakingAsset?.precision ?? 0, - }).toPrecision(), - ) - const fiatAmountAvailable = bnOrZero(cryptoAmountAvailablePrecision).times( - bnOrZero(marketData?.price), - ) - - const hasPendingUndelegation = Boolean( - undelegations && - undelegations.some( - undelegation => - dayjs().isAfter(dayjs(undelegation.completionTime).unix()) && - bnOrZero(undelegation.undelegationAmountCryptoBaseUnit).gt(0), - ), - ) - - const hasAvailableUndelegation = Boolean( - undelegations && - undelegations.some( - undelegation => - dayjs().isBefore(dayjs(undelegation.completionTime).unix()) && - bnOrZero(undelegation.undelegationAmountCryptoBaseUnit).gt(0), - ), - ) - - const claimDisabled = !canClaim || !(hasAvailableUndelegation || hasPendingUndelegation) - - const selectedLocale = useAppSelector(preferences.selectors.selectSelectedLocale) - const descriptionQuery = useGetAssetDescriptionQuery({ assetId: stakingAssetId, selectedLocale }) - - const underlyingAssetsCryptoPrecision = useMemo( - () => [ - { - ...stakingAsset, - cryptoBalancePrecision: cryptoAmountAvailablePrecision.toFixed(4), - allocationPercentage: '1', - }, - ], - [cryptoAmountAvailablePrecision, stakingAsset], - ) - const overviewMenu = useMemo( - () => [ - { - label: 'common.deposit', - icon: , - action: DefiAction.Deposit, - isDisabled: true, - toolTip: translate('defi.modals.foxyOverview.disabledTitle'), - }, - { - label: 'common.withdraw', - icon: , - action: DefiAction.Withdraw, - }, - { - label: 'common.claim', - icon: , - action: DefiAction.Claim, - variant: 'ghost-filled', - colorScheme: 'green', - isLoading: canClaim === null, - isDisabled: claimDisabled, - toolTip: translate('defi.modals.overview.noWithdrawals'), - }, - ], - [canClaim, claimDisabled, translate], - ) - - const overviewDescription = useMemo( - () => ({ - description: stakingAsset.description, - isLoaded: !descriptionQuery.isLoading, - isTrustedDescription: stakingAsset.isTrustedDescription, - }), - [descriptionQuery.isLoading, stakingAsset.description, stakingAsset.isTrustedDescription], - ) - - if (!foxyEarnOpportunityData) { - return ( - -
- -
-
- ) - } - - return ( - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Overview/WithdrawCard.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Overview/WithdrawCard.tsx deleted file mode 100644 index 470c87c858f..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Overview/WithdrawCard.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import type { ResponsiveValue, StackDirection } from '@chakra-ui/react' -import { Button, Skeleton, Stack, useColorModeValue } from '@chakra-ui/react' -import type { Asset } from '@shapeshiftoss/types' -import { BigAmount } from '@shapeshiftoss/utils' -import type { Property } from 'csstype' -import dayjs from 'dayjs' -import qs from 'qs' -import { useCallback, useMemo } from 'react' -import { FaArrowDown, FaArrowRight } from 'react-icons/fa' -import { useNavigate } from 'react-router-dom' - -import { Amount } from '@/components/Amount/Amount' -import { IconCircle } from '@/components/IconCircle' -import { Text } from '@/components/Text' -import type { TextPropTypes } from '@/components/Text/Text' -import { WalletActions } from '@/context/WalletProvider/actions' -import type { - DefiParams, - DefiQueryParams, -} from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { DefiAction } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' -import { useWallet } from '@/hooks/useWallet/useWallet' -import { bnOrZero } from '@/lib/bignumber/bignumber' -import type { UserUndelegation } from '@/state/slices/opportunitiesSlice/resolvers/foxy/types' - -type WithdrawCardProps = { - asset: Asset - undelegation: UserUndelegation | undefined - canClaimWithdraw: boolean | null -} - -const buttonAignItems = { base: 'flex-start', md: 'center' } -const flexDirectionMdRow: ResponsiveValue = { base: 'column', md: 'row' } -const stackFlexDirectionMdColumn: StackDirection = { - base: 'row', - md: 'column', -} -const stackGap = { base: 2, md: 0 } -const stackMarginLeft = { base: 0, md: 'auto' } -const stackTextAlign: ResponsiveValue = { base: 'left', md: 'right' } - -export const WithdrawCard = ({ asset, undelegation, canClaimWithdraw }: WithdrawCardProps) => { - const { location, query } = useBrowserRouter() - const { - state: { isConnected }, - dispatch, - } = useWallet() - const hasClaim = bnOrZero(undelegation?.undelegationAmountCryptoBaseUnit).gt(0) - const textColor = useColorModeValue('black', 'white') - const canClaimWithdrawLoading = canClaimWithdraw === null - const isUndelegationAvailable = - canClaimWithdraw && undelegation && dayjs().isAfter(dayjs.unix(undelegation.completionTime)) - const successColor = useColorModeValue('green.500', 'green.200') - const pendingColor = useColorModeValue('yellow.500', 'yellow.200') - const navigate = useNavigate() - - const handleWalletModalOpen = useCallback( - () => dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }), - [dispatch], - ) - - const handleClick = useCallback(() => { - if (!isConnected) return handleWalletModalOpen() - - navigate({ - pathname: location.pathname, - search: qs.stringify({ - ...query, - modal: DefiAction.Claim, - }), - }) - }, [handleWalletModalOpen, navigate, isConnected, location.pathname, query]) - - const availableDateTranslation: TextPropTypes['translation'] = useMemo( - () => [ - 'defi.modals.foxyOverview.availableDate', - { date: undelegation ? dayjs(dayjs.unix(undelegation.completionTime)).fromNow() : '' }, - ], - [undelegation], - ) - - if (!(undelegation && hasClaim)) return null - - return ( - - - {!hasClaim ? ( - - ) : ( - - )} - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/FoxyWithdraw.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/FoxyWithdraw.tsx deleted file mode 100644 index 884f735b17f..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/FoxyWithdraw.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Center } from '@chakra-ui/react' -import type { AccountId } from '@shapeshiftoss/caip' -import { KnownChainIds } from '@shapeshiftoss/types' -import qs from 'qs' -import { useCallback, useEffect, useMemo, useReducer } from 'react' -import { useTranslate } from 'react-polyglot' -import { useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' -import { getAddress } from 'viem' - -import { Approve } from './components/Approve' -import { Confirm } from './components/Confirm' -import { Status } from './components/Status' -import { Withdraw } from './components/Withdraw' -import { FoxyWithdrawActionType } from './WithdrawCommon' -import { WithdrawContext } from './WithdrawContext' -import { initialState, reducer } from './WithdrawReducer' - -import type { AccountDropdownProps } from '@/components/AccountDropdown/AccountDropdown' -import { CircularProgress } from '@/components/CircularProgress/CircularProgress' -import type { DefiStepProps } from '@/components/DeFi/components/Steps' -import { Steps } from '@/components/DeFi/components/Steps' -import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton' -import { DefiModalContent } from '@/features/defi/components/DefiModal/DefiModalContent' -import { DefiModalHeader } from '@/features/defi/components/DefiModal/DefiModalHeader' -import type { - DefiParams, - DefiQueryParams, -} from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { DefiAction, DefiStep } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' -import { useWallet } from '@/hooks/useWallet/useWallet' -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { - selectBip44ParamsByAccountId, - selectIsPortfolioLoading, - selectMarketDataByAssetIdUserCurrency, -} from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -export const FoxyWithdraw: React.FC<{ - onAccountIdChange: AccountDropdownProps['onChange'] - accountId: AccountId | undefined -}> = ({ onAccountIdChange: handleAccountIdChange, accountId }) => { - const foxyApi = getFoxyApi() - const translate = useTranslate() - const [state, dispatch] = useReducer(reducer, initialState) - const { query, location } = useBrowserRouter() - const { assetReference: foxyStakingContractAddress } = query - const { feeAssetId, underlyingAsset, underlyingAssetId, stakingAsset } = useFoxyQuery() - - const marketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, underlyingAssetId), - ) - - const feeMarketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, feeAssetId), - ) - const accountFilter = useMemo(() => ({ accountId: accountId ?? '' }), [accountId]) - const bip44Params = useAppSelector(state => selectBip44ParamsByAccountId(state, accountFilter)) - - // user info - const chainAdapterManager = getChainAdapterManager() - const chainAdapter = chainAdapterManager.get(KnownChainIds.EthereumMainnet) - const { state: walletState } = useWallet() - const loading = useSelector(selectIsPortfolioLoading) - - const navigate = useNavigate() - - useEffect(() => { - ;(async () => { - try { - if ( - !( - walletState.wallet && - foxyStakingContractAddress && - chainAdapter && - foxyApi && - bip44Params - ) - ) - return - const foxyOpportunity = await foxyApi.getFoxyOpportunityByStakingAddress( - getAddress(foxyStakingContractAddress), - ) - // Get foxy fee for instant sends - const foxyFeePercentage = await foxyApi.instantUnstakeFee({ - contractAddress: foxyStakingContractAddress, - }) - - dispatch({ - type: FoxyWithdrawActionType.SET_FOXY_FEE, - payload: bnOrZero(foxyFeePercentage).toString(), - }) - dispatch({ - type: FoxyWithdrawActionType.SET_OPPORTUNITY, - payload: foxyOpportunity, - }) - } catch (error) { - // TODO: handle client side errors - console.error(error) - } - })() - }, [foxyApi, bip44Params, chainAdapter, foxyStakingContractAddress, walletState.wallet]) - - const StepConfig: DefiStepProps = useMemo(() => { - return { - [DefiStep.Info]: { - label: translate('defi.steps.withdraw.info.title'), - description: translate('defi.steps.withdraw.info.yieldyDescription', { - asset: stakingAsset.symbol, - }), - component: ownProps => ( - - ), - }, - [DefiStep.Approve]: { - label: translate('defi.steps.approve.title'), - component: ownProps => , - props: { contractAddress: foxyStakingContractAddress }, - }, - [DefiStep.Confirm]: { - label: translate('defi.steps.confirm.title'), - component: ownProps => , - }, - [DefiStep.Status]: { - label: 'Status', - component: ownProps => , - }, - } - }, [accountId, handleAccountIdChange, foxyStakingContractAddress, translate, stakingAsset.symbol]) - - const handleBack = useCallback(() => { - navigate({ - pathname: location.pathname, - search: qs.stringify({ - ...query, - modal: DefiAction.Overview, - }), - }) - }, [navigate, query, location.pathname]) - - if (loading || !underlyingAsset || !marketData || !feeMarketData) - return ( -
- -
- ) - - return ( - - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawCommon.ts b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawCommon.ts deleted file mode 100644 index 7d5e57174ef..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawCommon.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { ChainId } from '@shapeshiftoss/caip' -import type { WithdrawType } from '@shapeshiftoss/types' - -import type { WithdrawValues } from '@/features/defi/components/Withdraw/Withdraw' -import type { BigNumber } from '@/lib/bignumber/bignumber' -import type { DefiType } from '@/state/slices/opportunitiesSlice/types' - -type SupportedFoxyOpportunity = { - type: DefiType - provider: string - version: string - contractAddress: string - rewardToken: string - stakingToken: string - chain: ChainId - tvl: BigNumber - apy: string - expired: boolean -} - -type EstimatedGas = { - estimatedGasCryptoBaseUnit?: string -} - -type FoxyWithdrawValues = WithdrawValues & - EstimatedGas & { - txStatus: string - usedGasFeeCryptoBaseUnit: string - withdrawType: WithdrawType - } - -export type FoxyWithdrawState = { - foxyOpportunity: SupportedFoxyOpportunity - approve: EstimatedGas - withdraw: FoxyWithdrawValues - loading: boolean - txid: string | null - foxyFeePercentage: string -} -export enum FoxyWithdrawActionType { - SET_OPPORTUNITY = 'SET_OPPORTUNITY', - SET_WITHDRAW = 'SET_WITHDRAW', - SET_APPROVE = 'SET_APPROVE', - SET_LOADING = 'SET_LOADING', - SET_TXID = 'SET_TXID', - SET_TX_STATUS = 'SET_TX_STATUS', - SET_FOXY_FEE = 'SET_FOXY_FEE', -} - -type SetVaultAction = { - type: FoxyWithdrawActionType.SET_OPPORTUNITY - payload: SupportedFoxyOpportunity | null -} - -type SetApprove = { - type: FoxyWithdrawActionType.SET_APPROVE - payload: EstimatedGas -} - -type SetWithdraw = { - type: FoxyWithdrawActionType.SET_WITHDRAW - payload: Partial -} - -type SetLoading = { - type: FoxyWithdrawActionType.SET_LOADING - payload: boolean -} - -type SetTxid = { - type: FoxyWithdrawActionType.SET_TXID - payload: string -} - -type SetFoxyFee = { - type: FoxyWithdrawActionType.SET_FOXY_FEE - payload: string -} - -export type FoxyWithdrawActions = - | SetVaultAction - | SetApprove - | SetWithdraw - | SetLoading - | SetTxid - | SetFoxyFee diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawContext.ts b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawContext.ts deleted file mode 100644 index 6b81821265f..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createContext } from 'react' - -import type { FoxyWithdrawActions, FoxyWithdrawState } from './WithdrawCommon' - -interface IWithdrawContext { - state: FoxyWithdrawState | null - dispatch: React.Dispatch | null -} - -export const WithdrawContext = createContext({ state: null, dispatch: null }) diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawReducer.ts b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawReducer.ts deleted file mode 100644 index 61b7ea34a18..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/WithdrawReducer.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { KnownChainIds, WithdrawType } from '@shapeshiftoss/types' - -import type { FoxyWithdrawActions, FoxyWithdrawState } from './WithdrawCommon' -import { FoxyWithdrawActionType } from './WithdrawCommon' - -import { bn } from '@/lib/bignumber/bignumber' -import { DefiType } from '@/state/slices/opportunitiesSlice/types' - -export const initialState: FoxyWithdrawState = { - txid: null, - foxyOpportunity: { - contractAddress: '', - stakingToken: '', - provider: '', - chain: KnownChainIds.EthereumMainnet, - type: DefiType.Staking, - expired: true, - version: '', - rewardToken: '', - tvl: bn(0), - apy: '', - }, - loading: false, - approve: {}, - withdraw: { - fiatAmount: '', - cryptoAmount: '', - slippage: '', - txStatus: 'pending', - usedGasFeeCryptoBaseUnit: '', - withdrawType: WithdrawType.DELAYED, - }, - foxyFeePercentage: '', -} - -export const reducer = (state: FoxyWithdrawState, action: FoxyWithdrawActions) => { - switch (action.type) { - case FoxyWithdrawActionType.SET_OPPORTUNITY: - return { ...state, foxyOpportunity: { ...state.foxyOpportunity, ...action.payload } } - case FoxyWithdrawActionType.SET_APPROVE: - return { ...state, approve: action.payload } - case FoxyWithdrawActionType.SET_WITHDRAW: - return { ...state, withdraw: { ...state.withdraw, ...action.payload } } - case FoxyWithdrawActionType.SET_LOADING: - return { ...state, loading: action.payload } - case FoxyWithdrawActionType.SET_TXID: - return { ...state, txid: action.payload } - case FoxyWithdrawActionType.SET_FOXY_FEE: - return { ...state, foxyFeePercentage: action.payload } - default: - return state - } -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx deleted file mode 100644 index edc6fc8d6de..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Approve.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import type { AccountId } from '@shapeshiftoss/caip' -import { fromAccountId } from '@shapeshiftoss/caip' -import { supportsETH } from '@shapeshiftoss/hdwallet-core/wallet' -import { BigAmount } from '@shapeshiftoss/utils' -import { useCallback, useContext, useMemo } from 'react' -import { useTranslate } from 'react-polyglot' - -import { FoxyWithdrawActionType } from '../WithdrawCommon' -import { WithdrawContext } from '../WithdrawContext' - -import type { StepComponentProps } from '@/components/DeFi/components/Steps' -import { Approve as ReusableApprove } from '@/features/defi/components/Approve/Approve' -import { ApprovePreFooter } from '@/features/defi/components/Approve/ApprovePreFooter' -import type { WithdrawValues } from '@/features/defi/components/Withdraw/Withdraw' -import { DefiAction, DefiStep } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { canCoverTxFees } from '@/features/defi/helpers/utils' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { useNotificationToast } from '@/hooks/useNotificationToast' -import { usePoll } from '@/hooks/usePoll/usePoll' -import { useWallet } from '@/hooks/useWallet/useWallet' -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { isSome } from '@/lib/utils' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { DefiProvider } from '@/state/slices/opportunitiesSlice/types' -import { selectBip44ParamsByAccountId } from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -type ApproveProps = StepComponentProps & { accountId: AccountId | undefined } - -export const Approve: React.FC = ({ accountId, onNext }) => { - const { poll } = usePoll() - const foxyApi = getFoxyApi() - const { state, dispatch } = useContext(WithdrawContext) - const estimatedGasCryptoBaseUnit = state?.approve.estimatedGasCryptoBaseUnit - const translate = useTranslate() - const { - underlyingAsset: asset, - rewardId, - feeAsset, - feeMarketData, - contractAddress, - } = useFoxyQuery() - const toast = useNotificationToast({ desktopPosition: 'top-right' }) - - const userAddress: string | undefined = accountId && fromAccountId(accountId).account - - const estimatedGasCryptoPrecision = useMemo( - () => - BigAmount.fromBaseUnit({ - value: estimatedGasCryptoBaseUnit ?? '0', - precision: feeAsset?.precision ?? 0, - }).toPrecision(), - [estimatedGasCryptoBaseUnit, feeAsset?.precision], - ) - - // user info - const { state: walletState } = useWallet() - - const accountFilter = useMemo(() => ({ accountId: accountId ?? '' }), [accountId]) - const bip44Params = useAppSelector(state => selectBip44ParamsByAccountId(state, accountFilter)) - - const getWithdrawGasEstimate = useCallback( - async (withdraw: WithdrawValues) => { - if (!(rewardId && userAddress && state?.withdraw && foxyApi && dispatch && bip44Params)) - return - try { - const feeDataEstimate = await foxyApi.estimateWithdrawFees({ - tokenContractAddress: rewardId, - contractAddress, - amountDesired: bnOrZero( - BigAmount.fromPrecision({ - value: withdraw.cryptoAmount ?? '0', - precision: asset.precision, - }).toBaseUnit(), - ), - userAddress, - type: state.withdraw.withdrawType, - bip44Params, - }) - - const { - chainSpecific: { gasPrice, gasLimit }, - } = feeDataEstimate.fast - - const returVal = bnOrZero(gasPrice).times(gasLimit).toFixed(0) - return returVal - } catch (error) { - console.error(error) - const fundsError = - error instanceof Error && error.message.includes('Not enough funds in reserve') - toast({ - description: fundsError - ? translate('defi.notEnoughFundsInReserve') - : translate('common.somethingWentWrong'), - title: translate('common.somethingWentWrong'), - status: 'error', - }) - } - }, - [ - rewardId, - userAddress, - state?.withdraw, - foxyApi, - dispatch, - bip44Params, - contractAddress, - asset.precision, - toast, - translate, - ], - ) - - const handleApprove = useCallback(async () => { - if ( - !( - rewardId && - walletState.wallet && - userAddress && - state?.withdraw && - foxyApi && - dispatch && - bip44Params && - feeAsset - ) - ) - return - - try { - if (!supportsETH(walletState.wallet)) - throw new Error(`handleApprove: wallet does not support ethereum`) - dispatch({ type: FoxyWithdrawActionType.SET_LOADING, payload: true }) - - await foxyApi.approve({ - tokenContractAddress: rewardId, - contractAddress, - userAddress, - wallet: walletState.wallet, - bip44Params, - }) - await poll({ - fn: () => - foxyApi.allowance({ - tokenContractAddress: rewardId, - contractAddress, - userAddress, - }), - validate: (result: string) => { - const allowance = bnOrZero( - BigAmount.fromBaseUnit({ - value: result ?? '0', - precision: asset.precision, - }).toPrecision(), - ) - return bnOrZero(allowance).gte(state.withdraw.cryptoAmount) - }, - interval: 15000, - maxAttempts: 60, - }) - // Get withdraw gas estimate - const estimatedGasCrypto = await getWithdrawGasEstimate(state.withdraw) - if (!estimatedGasCrypto) return - dispatch({ - type: FoxyWithdrawActionType.SET_WITHDRAW, - payload: { estimatedGasCryptoBaseUnit }, - }) - onNext(DefiStep.Confirm) - } catch (error) { - console.error(error) - toast({ - description: translate('common.transactionFailedBody'), - title: translate('common.transactionFailed'), - status: 'error', - }) - } finally { - dispatch({ type: FoxyWithdrawActionType.SET_LOADING, payload: false }) - } - }, [ - asset.precision, - bip44Params, - contractAddress, - dispatch, - estimatedGasCryptoBaseUnit, - feeAsset, - foxyApi, - getWithdrawGasEstimate, - onNext, - poll, - rewardId, - state?.withdraw, - toast, - translate, - userAddress, - walletState.wallet, - ]) - - const hasEnoughBalanceForGas = useMemo( - () => - isSome(estimatedGasCryptoBaseUnit) && - isSome(accountId) && - canCoverTxFees({ - feeAsset, - estimatedGasCryptoPrecision, - accountId, - }), - [estimatedGasCryptoBaseUnit, accountId, feeAsset, estimatedGasCryptoPrecision], - ) - - const preFooter = useMemo( - () => ( - - ), - [accountId, feeAsset, estimatedGasCryptoPrecision], - ) - - const handleCancel = useCallback(() => onNext(DefiStep.Info), [onNext]) - - if (!state || !dispatch) return null - - return ( - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx deleted file mode 100644 index ac3647e62b7..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Confirm.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { Alert, AlertIcon, Box, Stack } from '@chakra-ui/react' -import type { AccountId } from '@shapeshiftoss/caip' -import { fromAccountId } from '@shapeshiftoss/caip' -import { supportsETH } from '@shapeshiftoss/hdwallet-core/wallet' -import { WithdrawType } from '@shapeshiftoss/types' -import { BigAmount } from '@shapeshiftoss/utils' -import type { TransactionReceipt, TransactionReceiptParams } from 'ethers' -import isNil from 'lodash/isNil' -import { useCallback, useContext, useMemo } from 'react' -import { useTranslate } from 'react-polyglot' - -import { FoxyWithdrawActionType } from '../WithdrawCommon' -import { WithdrawContext } from '../WithdrawContext' - -import { Amount } from '@/components/Amount/Amount' -import { AssetIcon } from '@/components/AssetIcon' -import type { StepComponentProps } from '@/components/DeFi/components/Steps' -import { Row } from '@/components/Row/Row' -import { RawText, Text } from '@/components/Text' -import type { TextPropTypes } from '@/components/Text/Text' -import { Confirm as ReusableConfirm } from '@/features/defi/components/Confirm/Confirm' -import { Summary } from '@/features/defi/components/Summary' -import { DefiStep } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { usePoll } from '@/hooks/usePoll/usePoll' -import { useWallet } from '@/hooks/useWallet/useWallet' -import { bn, bnOrZero } from '@/lib/bignumber/bignumber' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { - selectBip44ParamsByAccountId, - selectPortfolioCryptoBalanceByFilter, -} from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -export const Confirm: React.FC = ({ - onNext, - accountId, -}) => { - const { poll } = usePoll() - const foxyApi = getFoxyApi() - const { state, dispatch } = useContext(WithdrawContext) - const translate = useTranslate() - const { stakingAsset, underlyingAsset, contractAddress, feeMarketData, rewardId, feeAsset } = - useFoxyQuery() - - // user info - const { state: walletState } = useWallet() - - const withdrawalFee = useMemo(() => { - return state?.withdraw.withdrawType === WithdrawType.INSTANT - ? bnOrZero( - bn(state?.withdraw.cryptoAmount ?? '0').times(state?.foxyFeePercentage ?? '0'), - ).toString() - : '0' - }, [state?.withdraw.withdrawType, state?.withdraw.cryptoAmount, state?.foxyFeePercentage]) - - const feeAssetBalanceFilter = useMemo( - () => ({ assetId: feeAsset?.assetId, accountId: accountId ?? '' }), - [accountId, feeAsset?.assetId], - ) - const feeAssetBalance = useAppSelector(s => - selectPortfolioCryptoBalanceByFilter(s, feeAssetBalanceFilter), - ) - - const accountAddress = useMemo( - () => (accountId ? fromAccountId(accountId).account : null), - [accountId], - ) - const accountFilter = useMemo(() => ({ accountId: accountId ?? '' }), [accountId]) - const bip44Params = useAppSelector(state => selectBip44ParamsByAccountId(state, accountFilter)) - - const handleConfirm = useCallback(async () => { - try { - if ( - state?.loading || - !( - state && - accountAddress && - rewardId && - walletState.wallet && - foxyApi && - dispatch && - bip44Params && - feeAsset - ) - ) - return - dispatch({ type: FoxyWithdrawActionType.SET_LOADING, payload: true }) - - if (!supportsETH(walletState.wallet)) - throw new Error(`handleConfirm: wallet does not support ethereum`) - - const txid = await foxyApi.withdraw({ - tokenContractAddress: rewardId, - userAddress: accountAddress, - contractAddress, - wallet: walletState.wallet, - amountDesired: bnOrZero( - BigAmount.fromPrecision({ - value: state.withdraw.cryptoAmount ?? '0', - precision: underlyingAsset.precision, - }).toBaseUnit(), - ), - type: state.withdraw.withdrawType, - bip44Params, - }) - dispatch({ type: FoxyWithdrawActionType.SET_TXID, payload: txid }) - onNext(DefiStep.Status) - - const transactionReceipt = await poll({ - fn: () => foxyApi.getTxReceipt({ txid }), - validate: (result: TransactionReceipt | null) => !isNil(result), - interval: 15000, - maxAttempts: 30, - }) - dispatch({ - type: FoxyWithdrawActionType.SET_WITHDRAW, - payload: { - txStatus: transactionReceipt?.status ? 'success' : 'failed', - usedGasFeeCryptoBaseUnit: bnOrZero( - // Types are drunk here, TransactionReceipt *does* implement TransactionReceiptParams but things are not narrowed down properly for some reason - (transactionReceipt as TransactionReceiptParams | null)?.effectiveGasPrice?.toString(), - ) - .times(bnOrZero(transactionReceipt?.gasUsed?.toString())) - .toString(), - }, - }) - dispatch({ type: FoxyWithdrawActionType.SET_LOADING, payload: false }) - } catch (error) { - console.error(error) - } - }, [ - state, - accountAddress, - rewardId, - walletState.wallet, - foxyApi, - dispatch, - bip44Params, - feeAsset, - contractAddress, - underlyingAsset.precision, - onNext, - poll, - ]) - - const estimatedGasBigAmount = useMemo( - () => - BigAmount.fromBaseUnit({ - value: state?.withdraw.estimatedGasCryptoBaseUnit ?? '0', - precision: feeAsset.precision, - }), - [state?.withdraw.estimatedGasCryptoBaseUnit, feeAsset.precision], - ) - - const hasEnoughBalanceForGas = feeAssetBalance.minus(estimatedGasBigAmount).gte(0) - - const handleCancel = useCallback(() => onNext(DefiStep.Info), [onNext]) - const notEnoughGasTranslation: TextPropTypes['translation'] = useMemo( - () => ['modals.confirm.notEnoughGas', { assetSymbol: feeAsset.symbol }], - [feeAsset.symbol], - ) - - if (!state || !dispatch) return null - - return ( - - - - - - - - - - {stakingAsset?.name} - - - - - - - - - - - {`${withdrawalFee} ${stakingAsset.symbol}`} - - - - - - - - - - - - - - - - - - - - - - {!hasEnoughBalanceForGas && ( - - - - - )} - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Status.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Status.tsx deleted file mode 100644 index cc0bf126ba6..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Status.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import { CheckIcon, CloseIcon, ExternalLinkIcon } from '@chakra-ui/icons' -import { Box, Button, Link, Stack } from '@chakra-ui/react' -import type { AccountId } from '@shapeshiftoss/caip' -import { fromAccountId } from '@shapeshiftoss/caip' -import { WithdrawType } from '@shapeshiftoss/types' -import { TxStatus } from '@shapeshiftoss/unchained-client' -import { BigAmount } from '@shapeshiftoss/utils' -import { useCallback, useContext, useMemo } from 'react' -import { useTranslate } from 'react-polyglot' -import { useNavigate } from 'react-router-dom' - -import { WithdrawContext } from '../WithdrawContext' - -import { Amount } from '@/components/Amount/Amount' -import { AssetIcon } from '@/components/AssetIcon' -import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis' -import { StatusTextEnum } from '@/components/RouteSteps/RouteSteps' -import { Row } from '@/components/Row/Row' -import { RawText, Text } from '@/components/Text' -import { Summary } from '@/features/defi/components/Summary' -import { TxStatus as TransactionStatus } from '@/features/defi/components/TxStatus/TxStatus' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { useSafeTxQuery } from '@/hooks/queries/useSafeTx' -import { bn, bnOrZero } from '@/lib/bignumber/bignumber' -import { getTxLink } from '@/lib/getTxLink' - -type StatusProps = { accountId: AccountId | undefined } - -const externalLinkIcon = - -export const Status: React.FC = ({ accountId }) => { - const { state, dispatch } = useContext(WithdrawContext) - const translate = useTranslate() - const { stakingAsset, underlyingAsset, feeAsset, feeMarketData } = useFoxyQuery() - const navigate = useNavigate() - - const { data: maybeSafeTx } = useSafeTxQuery({ - maybeSafeTxHash: state?.txid ?? undefined, - accountId, - }) - - const userAddress: string | undefined = accountId && fromAccountId(accountId).account - - const withdrawalFee = useMemo(() => { - return state?.withdraw.withdrawType === WithdrawType.INSTANT - ? bn(state.withdraw.cryptoAmount).times(state.foxyFeePercentage).toString() - : '0' - }, [state?.withdraw.withdrawType, state?.withdraw.cryptoAmount, state?.foxyFeePercentage]) - - const handleViewPosition = useCallback(() => { - navigate('/wallet/earn') - }, [navigate]) - - const handleCancel = useCallback(() => { - navigate(-1) - }, [navigate]) - - const { statusIcon, status, statusText, statusBg, statusBody } = useMemo(() => { - if (maybeSafeTx?.isQueuedSafeTx) - return { - statusIcon: , - status: TxStatus.Pending, - statusBg: 'transparent', - statusText: StatusTextEnum.pending, - statusBody: translate('common.safeProposalQueued', { - currentConfirmations: maybeSafeTx?.transaction?.confirmations?.length, - confirmationsRequired: maybeSafeTx?.transaction?.confirmationsRequired, - }), - } - - if (maybeSafeTx?.isExecutedSafeTx) { - return { - statusText: StatusTextEnum.success, - status: TxStatus.Confirmed, - statusIcon: , - statusBg: 'green.500', - statusBody: translate('modals.withdraw.status.success', { - opportunity: `${stakingAsset.symbol} Vault`, - }), - } - } - - switch (state?.withdraw.txStatus) { - case 'success': - return { - statusText: StatusTextEnum.success, - status: TxStatus.Confirmed, - statusIcon: , - statusBg: 'green.500', - statusBody: translate('modals.withdraw.status.success', { - opportunity: `${stakingAsset.symbol} Vault`, - }), - } - case 'failed': - return { - statusText: StatusTextEnum.failed, - status: TxStatus.Failed, - statusIcon: , - statusBg: 'red.500', - statusBody: translate('modals.withdraw.status.failed'), - } - default: - return { - statusIcon: , - status: TxStatus.Pending, - statusText: StatusTextEnum.pending, - statusBg: 'transparent', - statusBody: translate('modals.withdraw.status.pending'), - } - } - }, [ - maybeSafeTx?.isExecutedSafeTx, - maybeSafeTx?.isQueuedSafeTx, - maybeSafeTx?.transaction?.confirmations?.length, - maybeSafeTx?.transaction?.confirmationsRequired, - stakingAsset.symbol, - state?.withdraw.txStatus, - translate, - underlyingAsset?.icon, - ]) - - const txLink = useMemo(() => { - if (!feeAsset) return - if (!state?.txid) return - if (!accountId) return - - return getTxLink({ - txId: state?.txid ?? undefined, - defaultExplorerBaseUrl: feeAsset.explorerTxLink, - address: fromAccountId(accountId).account, - chainId: fromAccountId(accountId).chainId, - maybeSafeTx, - }) - }, [accountId, feeAsset, maybeSafeTx, state?.txid]) - - const usedGasOrEstimateCryptoPrecision = useMemo(() => { - if (maybeSafeTx?.transaction?.gasUsed) - return BigAmount.fromBaseUnit({ - value: maybeSafeTx.transaction.gasUsed, - precision: feeAsset.precision, - }).toPrecision() - if (state?.withdraw.usedGasFeeCryptoBaseUnit) - return BigAmount.fromBaseUnit({ - value: state.withdraw.usedGasFeeCryptoBaseUnit, - precision: feeAsset.precision, - }).toPrecision() - return BigAmount.fromBaseUnit({ - value: state?.withdraw.estimatedGasCryptoBaseUnit ?? '0', - precision: feeAsset.precision, - }).toPrecision() - }, [ - feeAsset.precision, - maybeSafeTx?.transaction?.gasUsed, - state?.withdraw.estimatedGasCryptoBaseUnit, - state?.withdraw.usedGasFeeCryptoBaseUnit, - ]) - - if (!state || !dispatch) return null - - return ( - - - - - - - - - - {stakingAsset.name} - - - - - - - - - - - {`${withdrawalFee} Foxy`} - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Withdraw.tsx deleted file mode 100644 index 02106bb62f0..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/Withdraw.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import type { AccountId } from '@shapeshiftoss/caip' -import { fromAccountId } from '@shapeshiftoss/caip' -import { WithdrawType } from '@shapeshiftoss/types' -import { BigAmount } from '@shapeshiftoss/utils' -import { useCallback, useContext, useMemo } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import { useTranslate } from 'react-polyglot' -import { useNavigate } from 'react-router-dom' - -import { FoxyWithdrawActionType } from '../WithdrawCommon' -import { WithdrawContext } from '../WithdrawContext' -import { WithdrawTypeField } from './WithdrawType' - -import type { AccountDropdownProps } from '@/components/AccountDropdown/AccountDropdown' -import type { StepComponentProps } from '@/components/DeFi/components/Steps' -import type { WithdrawValues } from '@/features/defi/components/Withdraw/Withdraw' -import { Field, Withdraw as ReusableWithdraw } from '@/features/defi/components/Withdraw/Withdraw' -import { DefiStep } from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useFoxyQuery } from '@/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery' -import { useNotificationToast } from '@/hooks/useNotificationToast' -import { BigNumber, bn, bnOrZero } from '@/lib/bignumber/bignumber' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { - selectBip44ParamsByAccountId, - selectMarketDataByAssetIdUserCurrency, - selectPortfolioCryptoBalanceByFilter, -} from '@/state/slices/selectors' -import { useAppSelector } from '@/state/store' - -export type FoxyWithdrawValues = { - [Field.WithdrawType]: WithdrawType -} & WithdrawValues - -const percentOptions = [0.25, 0.5, 0.75, 1] - -export const Withdraw: React.FC< - StepComponentProps & { - accountId: AccountId | undefined - onAccountIdChange: AccountDropdownProps['onChange'] - } -> = ({ accountId, onAccountIdChange: handleAccountIdChange, onNext }) => { - const foxyApi = getFoxyApi() - const { state, dispatch } = useContext(WithdrawContext) - const translate = useTranslate() - const navigate = useNavigate() - - const { - contractAddress, - underlyingAssetId: assetId, - underlyingAsset: asset, - rewardId, - stakingAsset, - } = useFoxyQuery() - - const toast = useNotificationToast({ desktopPosition: 'top-right' }) - - const methods = useForm({ mode: 'onChange' }) - const { setValue, watch } = methods - - const withdrawTypeValue = watch(Field.WithdrawType) - - const marketData = useAppSelector(state => selectMarketDataByAssetIdUserCurrency(state, assetId)) - - // user info - const filter = useMemo(() => ({ assetId, accountId: accountId ?? '' }), [assetId, accountId]) - const balance = useAppSelector(state => selectPortfolioCryptoBalanceByFilter(state, filter)) - - const cryptoAmountAvailable = balance - const fiatAmountAvailable = cryptoAmountAvailable.times(marketData?.price) - - const handlePercentClick = useCallback( - (percent: number) => { - const cryptoAmount = cryptoAmountAvailable - .times(percent) - .decimalPlaces(asset.precision, BigNumber.ROUND_DOWN) - const fiatAmount = cryptoAmount.times(marketData?.price) - setValue(Field.FiatAmount, fiatAmount.toPrecision(), { - shouldValidate: true, - }) - setValue(Field.CryptoAmount, cryptoAmount.toPrecision(), { - shouldValidate: true, - }) - }, - [asset.precision, cryptoAmountAvailable, marketData?.price, setValue], - ) - - const accountAddress = useMemo( - () => (accountId ? fromAccountId(accountId).account : null), - [accountId], - ) - const accountFilter = useMemo(() => ({ accountId: accountId ?? '' }), [accountId]) - const bip44Params = useAppSelector(state => selectBip44ParamsByAccountId(state, accountFilter)) - - const handleContinue = useCallback( - async (formValues: FoxyWithdrawValues) => { - if (!(accountAddress && dispatch && rewardId && foxyApi && bip44Params)) return - - const getApproveGasEstimateCryptoBaseUnit = async () => { - if (!accountAddress) return - - try { - const feeDataEstimate = await foxyApi.estimateApproveFees({ - tokenContractAddress: rewardId, - contractAddress, - userAddress: accountAddress, - }) - - const { - chainSpecific: { gasPrice, gasLimit }, - } = feeDataEstimate.fast - - return bn(gasPrice).times(gasLimit).toFixed(0) - } catch (error) { - console.error(error) - toast({ - description: translate('common.somethingWentWrongBody'), - title: translate('common.somethingWentWrong'), - status: 'error', - }) - } - } - - const getWithdrawGasEstimateCryptoBaseUnit = async (withdraw: FoxyWithdrawValues) => { - if (!accountAddress) return - - try { - const feeDataEstimate = await foxyApi.estimateWithdrawFees({ - tokenContractAddress: rewardId, - contractAddress, - amountDesired: bnOrZero( - BigAmount.fromPrecision({ - value: withdraw.cryptoAmount ?? '0', - precision: asset.precision, - }).toBaseUnit(), - ), - userAddress: accountAddress, - type: withdraw.withdrawType, - bip44Params, - }) - - const { - chainSpecific: { gasPrice, gasLimit }, - } = feeDataEstimate.fast - - return bn(gasPrice).times(gasLimit).toFixed(0) - } catch (error) { - console.error(error) - const fundsError = - error instanceof Error && error.message.includes('Not enough funds in reserve') - toast({ - description: fundsError - ? translate('defi.notEnoughFundsInReserve') - : translate('common.somethingWentWrong'), - title: translate('common.somethingWentWrong'), - status: 'error', - }) - } - } - - // set withdraw state for future use - dispatch({ - type: FoxyWithdrawActionType.SET_WITHDRAW, - payload: formValues, - }) - dispatch({ - type: FoxyWithdrawActionType.SET_LOADING, - payload: true, - }) - try { - // Check is approval is required for user address - const _allowance = await foxyApi.allowance({ - tokenContractAddress: rewardId, - contractAddress, - userAddress: accountAddress, - }) - - const allowance = BigAmount.fromBaseUnit({ - value: _allowance ?? '0', - precision: asset.precision, - }) - - // Skip approval step if user allowance is greater than or equal requested withdraw amount - if (allowance.gte(formValues.cryptoAmount)) { - const estimatedGasCryptoBaseUnit = await getWithdrawGasEstimateCryptoBaseUnit(formValues) - if (!estimatedGasCryptoBaseUnit) return - dispatch({ - type: FoxyWithdrawActionType.SET_WITHDRAW, - payload: { estimatedGasCryptoBaseUnit }, - }) - onNext(DefiStep.Confirm) - dispatch({ - type: FoxyWithdrawActionType.SET_LOADING, - payload: false, - }) - } else { - const estimatedGasCryptoBaseUnit = await getApproveGasEstimateCryptoBaseUnit() - if (!estimatedGasCryptoBaseUnit) return - dispatch({ - type: FoxyWithdrawActionType.SET_APPROVE, - payload: { estimatedGasCryptoBaseUnit }, - }) - onNext(DefiStep.Approve) - dispatch({ - type: FoxyWithdrawActionType.SET_LOADING, - payload: false, - }) - } - } catch (error) { - console.error(error) - dispatch({ - type: FoxyWithdrawActionType.SET_LOADING, - payload: false, - }) - toast({ - description: translate('common.somethingWentWrongBody'), - title: translate('common.somethingWentWrong'), - status: 'error', - }) - } - }, - [ - foxyApi, - asset.precision, - bip44Params, - contractAddress, - dispatch, - onNext, - rewardId, - accountAddress, - toast, - translate, - ], - ) - - const handleCancel = useCallback(() => navigate(-1), [navigate]) - - const validateCryptoAmount = useCallback( - (value: string) => { - const _value = bnOrZero(value) - const hasValidBalance = balance.gt(0) && _value.gt(0) && balance.gte(value) - if (_value.isEqualTo(0)) return '' - return hasValidBalance || 'common.insufficientFunds' - }, - [balance], - ) - - const validateFiatAmount = useCallback( - (value: string) => { - const fiat = balance.times(marketData?.price) - const _value = bnOrZero(value) - const hasValidBalance = fiat.gt(0) && _value.gt(0) && fiat.gte(value) - if (_value.isEqualTo(0)) return '' - return hasValidBalance || 'common.insufficientFunds' - }, - [balance, marketData?.price], - ) - - const fiatInputValidation = useMemo( - () => ({ - required: true, - validate: { validateFiatAmount }, - }), - [validateFiatAmount], - ) - - const cryptoInputValidation = useMemo( - () => ({ - required: true, - validate: { validateCryptoAmount }, - }), - [validateCryptoAmount], - ) - - if (!state || !dispatch) return null - - return ( - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/WithdrawType.tsx b/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/WithdrawType.tsx deleted file mode 100644 index 181cd8757fa..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/Withdraw/components/WithdrawType.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Button, ButtonGroup, Stack } from '@chakra-ui/react' -import type { Asset } from '@shapeshiftoss/types' -import { WithdrawType } from '@shapeshiftoss/types' -import { useCallback, useMemo } from 'react' -import { useController, useFormContext } from 'react-hook-form' -import { useTranslate } from 'react-polyglot' - -import { Amount } from '@/components/Amount/Amount' -import { FormField } from '@/components/DeFi/components/FormField' -import { Row } from '@/components/Row/Row' -import { RawText } from '@/components/Text' -import { bnOrZero } from '@/lib/bignumber/bignumber' - -type WithdrawTypeProps = { - asset: Asset - handlePercentClick: (arg: number) => void - feePercentage: string -} - -export const WithdrawTypeField: React.FC = ({ - handlePercentClick, - asset, - feePercentage, -}) => { - const { control, watch } = useFormContext() - const translate = useTranslate() - const { field: withdrawType } = useController({ - name: 'withdrawType', - control, - defaultValue: WithdrawType.DELAYED, - }) - - const cryptoAmount = watch('cryptoAmount') - - const handleClick = useCallback( - (value: WithdrawType) => { - if (value === WithdrawType.INSTANT) { - withdrawType.onChange(WithdrawType.INSTANT) - handlePercentClick(1) - } else { - withdrawType.onChange(WithdrawType.DELAYED) - } - }, - [handlePercentClick, withdrawType], - ) - - const withdrawalFee = useMemo(() => { - return withdrawType.value === WithdrawType.INSTANT - ? bnOrZero(cryptoAmount).times(feePercentage).toString() - : '0' - }, [cryptoAmount, feePercentage, withdrawType.value]) - - const handleInstantWithdrawClick = useCallback( - () => handleClick(WithdrawType.INSTANT), - [handleClick], - ) - - const handleDelayWithdrawClick = useCallback( - () => handleClick(WithdrawType.DELAYED), - [handleClick], - ) - - return ( - <> - - - - - - - - {translate('modals.withdraw.withdrawalFee')} - - - - - - ) -} diff --git a/src/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery.ts b/src/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery.ts deleted file mode 100644 index afe71961479..00000000000 --- a/src/features/defi/providers/foxy/components/FoxyManager/useFoxyQuery.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ASSET_REFERENCE, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import { useMemo } from 'react' - -import type { - DefiParams, - DefiQueryParams, -} from '@/features/defi/contexts/DefiManagerProvider/DefiCommon' -import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' -import { selectAssetById } from '@/state/slices/assetsSlice/selectors' -import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/marketDataSlice/selectors' -import { selectStakingOpportunityByFilter } from '@/state/slices/opportunitiesSlice/selectors' -import type { StakingId } from '@/state/slices/opportunitiesSlice/types' -import { useAppSelector } from '@/state/store' - -export const useFoxyQuery = () => { - const { query } = useBrowserRouter() - const { chainId, assetReference: contractAddress, assetNamespace } = query - const contractAssetId = toAssetId({ chainId, assetNamespace, assetReference: contractAddress }) - const opportunityMetadataFilter = useMemo( - () => ({ stakingId: contractAssetId as StakingId }), - [contractAssetId], - ) - const opportunityMetadata = useAppSelector(state => - selectStakingOpportunityByFilter(state, opportunityMetadataFilter), - ) - - const underlyingAssetId = opportunityMetadata?.underlyingAssetId ?? '' - const underlyingAsset = useAppSelector(state => selectAssetById(state, underlyingAssetId)) - const rewardId = fromAssetId(underlyingAssetId).assetReference - - // The Staking asset is one of the only underlying Asset Ids FOX - const stakingAssetId = opportunityMetadata?.underlyingAssetIds[0] ?? '' - const stakingAssetReference = fromAssetId(stakingAssetId).assetReference - const stakingAsset = useAppSelector(state => selectAssetById(state, stakingAssetId)) - - const feeAssetId = toAssetId({ - chainId, - assetNamespace: 'slip44', - assetReference: ASSET_REFERENCE.Ethereum, - }) - const feeAsset = useAppSelector(state => selectAssetById(state, feeAssetId)) - const feeMarketData = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, feeAssetId), - ) - - if (!stakingAsset) throw new Error(`Asset not found for AssetId ${stakingAssetId}`) - if (!feeAsset) throw new Error(`Fee asset not found for AssetId ${feeAssetId}`) - if (!underlyingAsset) throw new Error(`Asset not found for AssetId ${underlyingAssetId}`) - - return { - stakingAssetReference, - feeMarketData, - contractAddress, - stakingAsset, - feeAsset, - feeAssetId, - stakingAssetId, - underlyingAssetId, - underlyingAsset, - rewardId, - chainId, - contractAssetId, - } -} diff --git a/src/lib/investor/investor-foxy/api/api.ts b/src/lib/investor/investor-foxy/api/api.ts deleted file mode 100644 index 682af4b5d55..00000000000 --- a/src/lib/investor/investor-foxy/api/api.ts +++ /dev/null @@ -1,1115 +0,0 @@ -import type { ChainReference } from '@shapeshiftoss/caip' -import { CHAIN_REFERENCE } from '@shapeshiftoss/caip' -import type { - EvmBaseAdapter, - FeeDataEstimate, - GetFeeDataInput, -} from '@shapeshiftoss/chain-adapters' -import { CONTRACT_INTERACTION } from '@shapeshiftoss/chain-adapters' -import { - FOXY_ABI, - FOXY_STAKING_ABI, - LIQUIDITY_RESERVE_ABI, - TOKE_MANAGER_ABI, - TOKE_POOL_ABI, - TOKE_REWARD_HASH_ABI, -} from '@shapeshiftoss/contracts' -import type { EvmChainId } from '@shapeshiftoss/types' -import { KnownChainIds, WithdrawType } from '@shapeshiftoss/types' -import axios from 'axios' -import type { TransactionReceipt } from 'ethers' -import { ethers } from 'ethers' -import { toLower } from 'lodash' -import { erc20Abi, getAddress } from 'viem' - -import { tokeManagerAddress, tokePoolAddress, tokeRewardHashAddress } from '../constants' -import type { - AllowanceInput, - ApproveInput, - BalanceInput, - CanClaimWithdrawParams, - ClaimWithdrawal, - ContractAddressInput, - Epoch, - EstimateApproveFeesInput, - EstimateFeesTxInput, - EstimateWithdrawFeesInput, - FoxyAddressesType, - FoxyOpportunityInputData, - GetTokeRewardAmount, - SignAndBroadcastTx, - StakingContract, - TokeClaimIpfs, - TokenAddressInput, - TxInput, - TxInputWithoutAmount, - TxInputWithoutAmountAndWallet, - TxReceipt, - WithdrawInfo, - WithdrawInput, -} from './foxy-types' - -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { MAX_ALLOWANCE } from '@/lib/investor/constants/allowance' -import { DefiType } from '@/state/slices/opportunitiesSlice/types' - -export * from './foxy-types' - -type EthereumChainReference = typeof CHAIN_REFERENCE.EthereumMainnet - -export type ConstructorArgs = { - adapter: EvmBaseAdapter - providerUrl: string - provider: ethers.JsonRpcProvider - foxyAddresses: FoxyAddressesType - chainReference?: EthereumChainReference -} - -export const transformData = ({ tvl, apy, expired, ...contractData }: FoxyOpportunityInputData) => { - return { - type: DefiType.Staking, - provider: 'ShapeShift', - version: '1', - contractAddress: contractData.staking, - rewardToken: contractData.foxy, - stakingToken: contractData.fox, - chain: KnownChainIds.EthereumMainnet, - tvl, - apy, - expired, - } -} - -const TOKE_IPFS_URL = 'https://ipfs.tokemaklabs.xyz/ipfs' - -export class FoxyApi { - public adapter: EvmBaseAdapter - public provider: ethers.JsonRpcProvider - private providerUrl: string - private foxyStakingContracts: ethers.Contract[] - private liquidityReserveContracts: ethers.Contract[] - private readonly ethereumChainReference: ChainReference - private foxyAddresses: FoxyAddressesType - - constructor({ - adapter, - providerUrl, - foxyAddresses, - chainReference = CHAIN_REFERENCE.EthereumMainnet, - provider, - }: ConstructorArgs) { - this.adapter = adapter - this.provider = provider - this.foxyStakingContracts = foxyAddresses.map( - addresses => new ethers.Contract(addresses.staking, FOXY_STAKING_ABI, this.provider), - ) - this.liquidityReserveContracts = foxyAddresses.map( - addresses => - new ethers.Contract(addresses.liquidityReserve, LIQUIDITY_RESERVE_ABI, this.provider), - ) - this.ethereumChainReference = chainReference - this.providerUrl = providerUrl - this.foxyAddresses = foxyAddresses - } - - /** - * Very large amounts like those found in ERC20s with a precision of 18 get converted - * to exponential notation ('1.6e+21') in javascript. - * @param amount - */ - private normalizeAmount(amount: BigNumber): BigInt { - return BigInt(amount.toFixed()) - } - - // TODO(gomes): This is rank and should really belong in web for sanity sake. - private async signAndBroadcastTx(input: SignAndBroadcastTx): Promise { - const { payload, wallet, dryRun, receiverAddress } = input - - const { - chainSpecific: { gasPrice, gasLimit, maxFeePerGas, maxPriorityFeePerGas }, - } = payload.estimatedFees.fast - const shouldUseEIP1559Fees = - (await wallet.ethSupportsEIP1559()) && - maxFeePerGas !== undefined && - maxPriorityFeePerGas !== undefined - - const { txToSign } = await this.adapter.buildCustomTx({ - to: payload.to, - value: payload.value, - gasLimit, - wallet, - data: payload.data, - accountNumber: payload.bip44Params.accountNumber, - ...(shouldUseEIP1559Fees ? { maxFeePerGas, maxPriorityFeePerGas } : { gasPrice }), - }) - - const senderAddress = await this.adapter.getAddress({ - accountNumber: payload.bip44Params.accountNumber, - wallet, - }) - - if (wallet.supportsOfflineSigning()) { - const signedTx = await this.adapter.signTransaction({ txToSign, wallet }) - if (dryRun) return signedTx - try { - if (this.providerUrl.includes('localhost') || this.providerUrl.includes('127.0.0.1')) { - const sendSignedTx = await this.provider.broadcastTransaction(signedTx) - return sendSignedTx?.blockHash ?? '' - } - return this.adapter.broadcastTransaction({ - senderAddress, - receiverAddress, - hex: signedTx, - }) - } catch (e) { - throw new Error(`Failed to broadcast: ${e}`) - } - } else if (wallet.supportsBroadcast() && this.adapter.signAndBroadcastTransaction) { - if (dryRun) { - throw new Error(`Cannot perform a dry run with wallet of type ${wallet.getVendor()}`) - } - return this.adapter.signAndBroadcastTransaction({ - senderAddress, - receiverAddress, - signTxInput: { txToSign, wallet }, - }) - } else { - throw new Error('Invalid HDWallet configuration ') - } - } - - checksumAddress(address: string): string { - // viem always returns checksum addresses from getAddress() calls - return getAddress(address) - } - - private verifyAddresses(addresses: string[]) { - addresses.forEach(address => { - this.checksumAddress(address) - }) - } - - private getStakingContract(contractAddress: string): ethers.Contract { - const stakingContract = this.foxyStakingContracts.find( - // This can be string | Addressable, where Addressable is an object containing getAddress() - // for ENS names. We can safely narrow it down to a string, as we do not instantiate contracts with an ens name. - item => toLower(item.target as string) === toLower(contractAddress), - ) - if (!stakingContract) throw new Error('Not a valid contract address') - return stakingContract - } - - private getLiquidityReserveContract(liquidityReserveAddress: string): ethers.Contract { - const liquidityReserveContract = this.liquidityReserveContracts.find( - // This can be string | Addressable, where Addressable is an object containing getAddress() - // for ENS names. We can safely narrow it down to a string, as we do not instantiate contracts with an ens name. - item => toLower(item.target as string) === toLower(liquidityReserveAddress), - ) - if (!liquidityReserveContract) throw new Error('Not a valid reserve contract address') - return liquidityReserveContract - } - - async getFoxyOpportunities() { - try { - const opportunities = await Promise.all( - this.foxyAddresses.map(async addresses => { - const stakingContract = this.foxyStakingContracts.find( - // This can be string | Addressable, where Addressable is an object containing getAddress() - // for ENS names. We can safely narrow it down to a string, as we do not instantiate contracts with an ens name. - item => toLower(item.target as string) === toLower(addresses.staking), - ) - try { - const expired = await stakingContract?.pauseStaking() - const tvl = await this.tvl({ tokenContractAddress: addresses.foxy }) - const apy = this.apy() - return transformData({ ...addresses, expired, tvl, apy }) - } catch (e) { - throw new Error(`Failed to get contract data ${e}`) - } - }), - ) - return opportunities - } catch (e) { - throw new Error(`getFoxyOpportunities Error: ${e}`) - } - } - - async getFoxyOpportunityByStakingAddress(stakingAddress: string) { - this.verifyAddresses([stakingAddress]) - const addresses = this.foxyAddresses.find(item => { - return item.staking === stakingAddress - }) - if (!addresses) throw new Error('Not a valid address') - - const stakingContract = this.getStakingContract(addresses.staking) - - try { - const expired = await stakingContract.pauseStaking() - const tvl = await this.tvl({ tokenContractAddress: addresses.foxy }) - const apy = this.apy() - return transformData({ ...addresses, tvl, apy, expired }) - } catch (e) { - throw new Error(`Failed to get contract data ${e}`) - } - } - - getTxReceipt({ txid }: TxReceipt): Promise { - if (!txid) throw new Error('Must pass txid') - return this.provider.getTransactionReceipt(txid) - } - - async estimateClaimWithdrawFees( - input: ClaimWithdrawal, - ): Promise> { - const { claimAddress, userAddress, contractAddress } = input - const addressToClaim = claimAddress ?? userAddress - this.verifyAddresses([addressToClaim, userAddress, contractAddress]) - - const stakingContract = this.getStakingContract(contractAddress) - - try { - const data = stakingContract.interface.encodeFunctionData('claimWithdraw', [addressToClaim]) - const getFeeDataInput: GetFeeDataInput = { - to: contractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async estimateSendWithdrawalRequestsFees( - input: TxInputWithoutAmountAndWallet, - ): Promise> { - const { userAddress, contractAddress } = input - this.verifyAddresses([userAddress, contractAddress]) - - const stakingContract = this.getStakingContract(contractAddress) - - try { - const data = stakingContract.interface.encodeFunctionData('sendWithdrawalRequests', []) - const getFeeDataInput: GetFeeDataInput = { - to: contractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async estimateAddLiquidityFees( - input: EstimateFeesTxInput, - ): Promise> { - const { amountDesired, userAddress, contractAddress } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!amountDesired.gt(0)) throw new Error('Must send valid amount') - - const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - - try { - const data = liquidityReserveContract.interface.encodeFunctionData('addLiquidity', [ - this.normalizeAmount(amountDesired), - ]) - const getFeeDataInput: GetFeeDataInput = { - to: contractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async estimateRemoveLiquidityFees( - input: EstimateFeesTxInput, - ): Promise> { - const { amountDesired, userAddress, contractAddress } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!amountDesired.gt(0)) throw new Error('Must send valid amount') - - const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - - try { - const data = await liquidityReserveContract.encodeFunctionData('removeLiquidity', [ - this.normalizeAmount(amountDesired), - ]) - - const getFeeDataInput: GetFeeDataInput = { - to: contractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async estimateWithdrawFees( - input: EstimateWithdrawFeesInput, - ): Promise> { - const { amountDesired, userAddress, contractAddress, type } = input - this.verifyAddresses([userAddress, contractAddress]) - - const stakingContract = this.getStakingContract(contractAddress) - - const isDelayed = type === WithdrawType.DELAYED && amountDesired - if (isDelayed && !amountDesired.gt(0)) throw new Error('Must send valid amount') - - try { - const data = isDelayed - ? stakingContract.interface.encodeFunctionData('unstake(uint256,bool)', [ - this.normalizeAmount(amountDesired), - true, - ]) - : stakingContract.interface.encodeFunctionData('instantUnstake', [true]) - - const getFeeDataInput: GetFeeDataInput = { - to: contractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async estimateApproveFees( - input: EstimateApproveFeesInput, - ): Promise> { - const { userAddress, tokenContractAddress, contractAddress } = input - this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - - const depositTokenContract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) - - try { - const data = depositTokenContract.interface.encodeFunctionData('approve', [ - contractAddress, - MAX_ALLOWANCE, - ]) - const getFeeDataInput: GetFeeDataInput = { - to: tokenContractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async estimateDepositFees( - input: EstimateFeesTxInput, - ): Promise> { - const { amountDesired, userAddress, contractAddress } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!amountDesired.gt(0)) throw new Error('Must send valid amount') - - const stakingContract = this.getStakingContract(contractAddress) - - try { - const data = stakingContract.interface.encodeFunctionData('stake(uint256)', [ - this.normalizeAmount(amountDesired), - ]) - - const getFeeDataInput: GetFeeDataInput = { - to: contractAddress, - value: '0', - chainSpecific: { - data, - from: userAddress, - }, - } - const feeData = await this.adapter.getFeeData(getFeeDataInput) - - const { - chainSpecific: { gasLimit: gasLimitBase }, - } = feeData.fast - const safeGasLimit = bnOrZero(gasLimitBase).times('1.05').toFixed(0) - feeData.fast.chainSpecific.gasLimit = safeGasLimit - - return feeData - } catch (e) { - throw new Error(`Failed to get gas ${e}`) - } - } - - async approve(input: ApproveInput): Promise { - const { - amount, - bip44Params, - dryRun = false, - tokenContractAddress, - userAddress, - wallet, - contractAddress, - } = input - this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - if (!wallet) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateApproveFees(input) - const depositTokenContract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) - const data: string = depositTokenContract.interface.encodeFunctionData('approve', [ - contractAddress, - amount ? this.normalizeAmount(bnOrZero(amount)) : MAX_ALLOWANCE, - ]) - - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: tokenContractAddress, - value: '0', - } - return this.signAndBroadcastTx({ - payload, - wallet, - dryRun, - receiverAddress: CONTRACT_INTERACTION, - }) - } - - async allowance(input: AllowanceInput): Promise { - const { userAddress, tokenContractAddress, contractAddress } = input - this.verifyAddresses([userAddress, contractAddress, tokenContractAddress]) - - const depositTokenContract: ethers.Contract = new ethers.Contract( - tokenContractAddress, - erc20Abi, - this.provider, - ) - - const allowance = await depositTokenContract.allowance(userAddress, contractAddress) - return allowance.toString() - } - - async deposit(input: TxInput): Promise { - const { - amountDesired, - bip44Params, - dryRun = false, - contractAddress, - userAddress, - wallet, - } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!amountDesired.gt(0)) throw new Error('Must send valid amount') - if (!wallet) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateDepositFees(input) - - const stakingContract = this.getStakingContract(contractAddress) - - const data = stakingContract.interface.encodeFunctionData('stake(uint256,address)', [ - this.normalizeAmount(amountDesired), - userAddress, - ]) - - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: contractAddress, - value: '0', - } - return this.signAndBroadcastTx({ - payload, - wallet, - dryRun, - receiverAddress: CONTRACT_INTERACTION, - }) - } - - async withdraw(input: WithdrawInput): Promise { - const { - amountDesired, - bip44Params, - dryRun = false, - contractAddress, - userAddress, - type, - wallet, - } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!wallet) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateWithdrawFees(input) - - const stakingContract = this.getStakingContract(contractAddress) - - const isDelayed = type === WithdrawType.DELAYED && amountDesired - if (isDelayed && !amountDesired.gt(0)) throw new Error('Must send valid amount') - - const stakingContractCallInput: Parameters< - typeof stakingContract.interface.encodeFunctionData - > = isDelayed - ? ['unstake(uint256,bool)', [this.normalizeAmount(amountDesired), true]] - : ['instantUnstake', ['true']] - const data: string = stakingContract.interface.encodeFunctionData(...stakingContractCallInput) - - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: contractAddress, - value: '0', - } - return this.signAndBroadcastTx({ payload, wallet, dryRun, receiverAddress: userAddress }) - } - - async canClaimWithdraw(input: CanClaimWithdrawParams): Promise { - const { userAddress, contractAddress } = input - const tokeManagerContract = new ethers.Contract( - tokeManagerAddress, - TOKE_MANAGER_ABI, - this.provider, - ) - const tokePoolContract = new ethers.Contract(tokePoolAddress, TOKE_POOL_ABI, this.provider) - const stakingContract = this.getStakingContract(contractAddress) - - const coolDownInfo = await (async () => { - try { - const coolDown = await stakingContract.coolDownInfo(userAddress) - return { - ...coolDown, - endEpoch: coolDown.expiry, - } - } catch (e) { - console.error(e, 'failed to get coolDowninfo') - } - })() - - const epoch: Epoch = await (async () => { - try { - return (await stakingContract.epoch()).toObject() - } catch (e) { - throw new Error(`Failed to get epoch: ${e}`) - } - })() - - const requestedWithdrawals: { - minCycle?: BigInt - amount?: BigInt - } = await (() => { - try { - return tokePoolContract.requestedWithdrawals(stakingContract.target) - } catch (e) { - console.error(e, 'failed to get requestedWithdrawals') - return {} - } - })() - - const currentCycleIndex: BigInt = await (() => { - try { - return tokeManagerContract.getCurrentCycleIndex() - } catch (e) { - console.error(e, 'failed to get currentCycleIndex') - return BigInt(0) - } - })() - const withdrawalAmount: BigInt = await (() => { - try { - return stakingContract.withdrawalAmount() - } catch (e) { - console.error(e, 'failed to get currentCycleIndex') - return BigInt(0) - } - })() - - const currentBlock = await this.provider.getBlockNumber() - - const epochExpired = bnOrZero(epoch.number.toString()).gte(coolDownInfo.endEpoch.toString()) - const coolDownValid = - !bnOrZero(coolDownInfo.endEpoch).isZero() && !bnOrZero(coolDownInfo.amount).isZero() - - const pastTokeCycleIndex = bnOrZero(requestedWithdrawals.minCycle?.toString()).lte( - currentCycleIndex.toString(), - ) - const stakingTokenAvailableWithTokemak = bnOrZero(requestedWithdrawals.amount?.toString()).plus( - withdrawalAmount.toString(), - ) - const stakingTokenAvailable = bnOrZero(withdrawalAmount.toString()).gte(coolDownInfo.amount) - const validCycleAndAmount = - (pastTokeCycleIndex && stakingTokenAvailableWithTokemak.gte(coolDownInfo.amount)) || - stakingTokenAvailable - - const epochsLeft = bnOrZero(coolDownInfo.endEpoch.toString()).minus(epoch.number.toString()) - const blocksLeftInCurrentEpoch = - epochsLeft.gt(0) && bnOrZero(epoch.endBlock.toString()).gt(currentBlock) - ? bnOrZero(epoch.endBlock.toString()).minus(currentBlock).toNumber() - : 0 // calculate time remaining in current epoch - const blocksLeftInFutureEpochs = epochsLeft.minus(1).gt(0) - ? epochsLeft.minus(1).times(epoch.length.toString()).toNumber() - : 0 - - return ( - (!blocksLeftInCurrentEpoch && !blocksLeftInFutureEpochs) || // satisfying getTimeUntilClaimable constraints - (epochExpired && coolDownValid && validCycleAndAmount) - ) - } - - async claimWithdraw(input: ClaimWithdrawal): Promise { - const { - bip44Params, - dryRun = false, - contractAddress, - userAddress, - claimAddress, - wallet, - } = input - const addressToClaim = claimAddress ?? userAddress - this.verifyAddresses([userAddress, contractAddress, addressToClaim]) - if (!wallet) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateClaimWithdrawFees(input) - - const stakingContract = this.getStakingContract(contractAddress) - - const canClaim = await this.canClaimWithdraw({ userAddress, contractAddress }) - if (!canClaim) throw new Error('Not ready to claim') - - const data: string = stakingContract.interface.encodeFunctionData('claimWithdraw', [ - addressToClaim, - ]) - - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: contractAddress, - value: '0', - } - return this.signAndBroadcastTx({ payload, wallet, dryRun, receiverAddress: userAddress }) - } - - async canSendWithdrawalRequest(input: StakingContract): Promise { - const { stakingContract } = input - const tokeManagerContract = new ethers.Contract( - tokeManagerAddress, - TOKE_MANAGER_ABI, - this.provider, - ) - - const requestWithdrawalAmount = await (async () => { - try { - return (await stakingContract.requestWithdrawalAmount()).toString() - } catch (e) { - console.error(e, 'failed to get requestWithdrawalAmount') - return 0 - } - })() - - const timeLeftToRequestWithdrawal: string = await (async () => { - try { - return (await stakingContract.timeLeftToRequestWithdrawal()).toString() - } catch (e) { - console.error(e, 'failed to get timeLeftToRequestWithdrawal') - return '0' - } - })() - - const lastTokeCycleIndex: string = await (async () => { - try { - return (await stakingContract.lastTokeCycleIndex()).toString() - } catch (err) { - console.error(err, 'failed to get lastTokeCycleIndex') - return '0' - } - })() - - const duration: string = await (async () => { - try { - return (await tokeManagerContract.getCycleDuration()).toString() - } catch (e) { - console.error(e, 'failed to get cycleDuration') - return '0' - } - })() - - const currentCycleIndex: string = await (async () => { - try { - return (await tokeManagerContract.getCurrentCycleIndex()).toString() - } catch (e) { - console.error(e, 'failed to get currentCycleIndex') - return '0' - } - })() - - const currentCycleStart: string = await (async () => { - try { - return (await tokeManagerContract.getCurrentCycle()).toString() - } catch (e) { - console.error(e, 'failed to get currentCycle') - return '0' - } - })() - - const nextCycleStart = bnOrZero(currentCycleStart).plus(duration) - - const blockNumber = await this.provider.getBlockNumber() - const timestamp = (await this.provider.getBlock(blockNumber))?.timestamp - - const isTimeToRequest = bnOrZero(timestamp) - .plus(timeLeftToRequestWithdrawal) - .gte(nextCycleStart) - const isCorrectIndex = bnOrZero(currentCycleIndex).gt(lastTokeCycleIndex) - const hasAmount = bnOrZero(requestWithdrawalAmount).gt(0) - - return isTimeToRequest && isCorrectIndex && hasAmount - } - - async sendWithdrawalRequests(input: TxInputWithoutAmount): Promise { - const { bip44Params, dryRun = false, contractAddress, userAddress, wallet } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!wallet || !contractAddress) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateSendWithdrawalRequestsFees(input) - - const stakingContract = this.getStakingContract(contractAddress) - - const canSendRequest = await this.canSendWithdrawalRequest({ stakingContract }) - if (!canSendRequest) throw new Error('Not ready to send request') - - const data: string = stakingContract.interface.encodeFunctionData('sendWithdrawalRequests') - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: contractAddress, - value: '0', - } - return this.signAndBroadcastTx({ payload, wallet, dryRun, receiverAddress: userAddress }) - } - - // not a user facing function - // utility function for the dao to add liquidity to the lrContract for instantUnstaking - async addLiquidity(input: TxInput): Promise { - const { - amountDesired, - bip44Params, - dryRun = false, - contractAddress, - userAddress, - wallet, - } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!amountDesired.gt(0)) throw new Error('Must send valid amount') - - if (!wallet) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateAddLiquidityFees(input) - const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - - const data: string = liquidityReserveContract.interface.encodeFunctionData('addLiquidity', [ - this.normalizeAmount(amountDesired), - ]) - - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: contractAddress, - value: '0', - } - return this.signAndBroadcastTx({ - payload, - wallet, - dryRun, - receiverAddress: CONTRACT_INTERACTION, - }) - } - - // not a user facing function - // utility function for the dao to remove liquidity to the lrContract for instantUnstaking - async removeLiquidity(input: TxInput): Promise { - const { - amountDesired, - bip44Params, - dryRun = false, - contractAddress, - userAddress, - wallet, - } = input - this.verifyAddresses([userAddress, contractAddress]) - if (!amountDesired.gt(0)) throw new Error('Must send valid amount') - if (!wallet) throw new Error('Missing inputs') - - const estimatedFees = await this.estimateRemoveLiquidityFees(input) - - const liquidityReserveContract = this.getLiquidityReserveContract(contractAddress) - - const data: string = liquidityReserveContract.interface.encodeFunctionData('removeLiquidity', [ - this.normalizeAmount(amountDesired), - ]) - - const chainReferenceAsNumber = Number(this.ethereumChainReference) - const payload = { - bip44Params, - chainId: chainReferenceAsNumber, - data, - estimatedFees, - to: contractAddress, - value: '0', - } - return this.signAndBroadcastTx({ payload, wallet, dryRun, receiverAddress: userAddress }) - } - - // returns time when the users withdraw request is claimable - async getTimeUntilClaimable(input: TxInputWithoutAmountAndWallet): Promise { - const { contractAddress, userAddress } = input - this.verifyAddresses([userAddress, contractAddress]) - - const stakingContract = this.getStakingContract(contractAddress) - - let coolDownInfo - try { - const coolDown = await stakingContract.coolDownInfo(userAddress) - coolDownInfo = { - ...coolDown, - endEpoch: coolDown.expiry, - } - } catch (e) { - throw new Error(`Failed to get coolDowninfo: ${e}`) - } - - const epoch: Epoch = await (async () => { - try { - return (await stakingContract.epoch()).toObject() - } catch (e) { - throw new Error(`Failed to get epoch: ${e}`) - } - })() - - let currentBlock - try { - currentBlock = await this.provider.getBlockNumber() - } catch (e) { - throw new Error(`Failed to get block number: ${e}`) - } - const epochsLeft = bnOrZero(coolDownInfo.endEpoch.toString()).minus(epoch.number.toString()) // epochs left until can claim - const blocksLeftInCurrentEpoch = - epochsLeft.gt(0) && bnOrZero(epoch.endBlock.toString()).gt(currentBlock) - ? bnOrZero(epoch.endBlock.toString()).minus(currentBlock).toNumber() - : 0 // calculate time remaining in current epoch - const blocksLeftInFutureEpochs = epochsLeft.minus(1).gt(0) - ? epochsLeft.minus(1).times(epoch.length.toString()).toNumber() - : 0 // don't count current epoch - const blocksUntilClaimable = bnOrZero(blocksLeftInCurrentEpoch).plus(blocksLeftInFutureEpochs) // total blocks left until can claim - const secondsUntilClaimable = blocksUntilClaimable.times(13) // average block time is 13 seconds to get total seconds - const currentDate = new Date() - currentDate.setSeconds(secondsUntilClaimable.plus(currentDate.getSeconds()).toNumber()) - - return currentDate.toString() - } - - async balance(input: BalanceInput): Promise { - const { tokenContractAddress, userAddress } = input - this.verifyAddresses([userAddress, tokenContractAddress]) - - const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) - try { - const balance = await contract.balanceOf(userAddress) - return bnOrZero(balance.toString()) - } catch (e) { - throw new Error(`Failed to get balance: ${e}`) - } - } - - async instantUnstakeFee(input: ContractAddressInput): Promise { - const { contractAddress } = input - this.verifyAddresses([contractAddress]) - const stakingContract = this.getStakingContract(contractAddress) - - let liquidityReserveAddress - try { - liquidityReserveAddress = await stakingContract.LIQUIDITY_RESERVE() - } catch (e) { - throw new Error(`Failed to get liquidityReserve address ${e}`) - } - const liquidityReserveContract = this.getLiquidityReserveContract(liquidityReserveAddress) - try { - const feeInBasisPoints = bnOrZero((await liquidityReserveContract.fee()).toString()) - return feeInBasisPoints.div(10000) // convert from basis points to decimal percentage - } catch (e) { - throw new Error(`Failed to get instantUnstake fee ${e}`) - } - } - - async totalSupply({ tokenContractAddress }: TokenAddressInput): Promise { - this.verifyAddresses([tokenContractAddress]) - const contract = new ethers.Contract(tokenContractAddress, erc20Abi, this.provider) - - try { - const totalSupply = await contract.totalSupply() - return bnOrZero(totalSupply.toString()) - } catch (e) { - throw new Error(`Failed to get totalSupply: ${e}`) - } - } - - // TODO: use tokemak's api to get apy when they build it - apy(): string { - return '.15' - } - - async tvl(input: TokenAddressInput): Promise { - const { tokenContractAddress } = input - this.verifyAddresses([tokenContractAddress]) - const contract = new ethers.Contract(tokenContractAddress, FOXY_ABI, this.provider) - - try { - const balance = await contract.circulatingSupply() - return bnOrZero(balance.toString()) - } catch (e) { - throw new Error(`Failed to get tvl: ${e}`) - } - } - - async getWithdrawInfo(input: TxInputWithoutAmountAndWallet): Promise { - const { contractAddress, userAddress } = input - this.verifyAddresses([userAddress, contractAddress]) - const stakingContract = this.getStakingContract(contractAddress) - - const coolDownInfo: [amount: string, gons: string, expiry: string] = ( - await stakingContract.coolDownInfo(userAddress) - ).map((info: BigInt) => info.toString()) - const releaseTime = await this.getTimeUntilClaimable(input) - - const [amount, gons, expiry] = coolDownInfo - return { - amount, - gons, - expiry, - releaseTime, - } - } - - async getClaimFromTokemakArgs(input: ContractAddressInput): Promise { - const { contractAddress } = input - const rewardHashContract = new ethers.Contract( - tokeRewardHashAddress, - TOKE_REWARD_HASH_ABI, - this.provider, - ) - const latestCycleIndex = await (() => { - try { - return rewardHashContract.latestCycleIndex() - } catch (e) { - throw new Error(`Failed to get latestCycleIndex, ${e}`) - } - })() - const cycleHashes = await (() => { - try { - return rewardHashContract.cycleHashes(latestCycleIndex) - } catch (e) { - throw new Error(`Failed to get latestCycleIndex, ${e}`) - } - })() - - try { - const { latestClaimable } = cycleHashes - const response = await axios.get( - `${TOKE_IPFS_URL}/${latestClaimable}/${contractAddress.toLowerCase()}.json`, - ) - const { - data: { payload, signature }, - } = response - - const v = signature.v - const r = signature.r - const s = signature.s - return { - v, - r, - s, - recipient: payload, - } - } catch (e) { - throw new Error(`Failed to get information from Tokemak ipfs ${e}`) - } - } -} diff --git a/src/lib/investor/investor-foxy/api/foxy-types.ts b/src/lib/investor/investor-foxy/api/foxy-types.ts deleted file mode 100644 index 5a26d43c5c2..00000000000 --- a/src/lib/investor/investor-foxy/api/foxy-types.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { ContractInteraction, FeeDataEstimate } from '@shapeshiftoss/chain-adapters' -import type { ETHWallet } from '@shapeshiftoss/hdwallet-core' -import type { Bip44Params, KnownChainIds, WithdrawType } from '@shapeshiftoss/types' -import type { Contract } from 'ethers' - -export type FoxyAddressesType = { - staking: string - liquidityReserve: string - fox: string - foxy: string - version: number -}[] - -export type TxReceipt = { txid: string } - -export type AllowanceInput = { - tokenContractAddress: string - contractAddress: string - userAddress: string -} - -export type ApproveInput = { - amount?: string - bip44Params: Bip44Params - dryRun?: boolean - tokenContractAddress: string - contractAddress: string - userAddress: string - wallet: ETHWallet -} - -export type EstimateApproveFeesInput = Pick< - ApproveInput, - 'userAddress' | 'tokenContractAddress' | 'contractAddress' -> - -export type TxInput = { - bip44Params: Bip44Params - dryRun?: boolean - tokenContractAddress?: string - userAddress: string - contractAddress: string - wallet: ETHWallet - amountDesired: BigNumber -} - -export type TxInputWithoutAmount = Pick> - -export type TxInputWithoutAmountAndWallet = Pick< - TxInputWithoutAmount, - Exclude -> - -export type WithdrawInput = Omit & { - type: WithdrawType - amountDesired?: BigNumber -} - -export type EstimateWithdrawFeesInput = Omit - -export type FoxyOpportunityInputData = { - tvl: BigNumber - apy: string - expired: boolean - staking: string - foxy: string - fox: string - liquidityReserve: string -} - -export type EstimateFeesTxInput = Pick< - TxInput, - 'tokenContractAddress' | 'contractAddress' | 'userAddress' | 'amountDesired' -> - -export type BalanceInput = { - userAddress: string - tokenContractAddress: string -} - -export type TokenAddressInput = { - tokenContractAddress: string -} - -export type ContractAddressInput = { - contractAddress: string -} - -export type ClaimWithdrawal = TxInputWithoutAmount & { - claimAddress?: string -} - -export type WithdrawInfo = { - amount: string - gons: string - expiry: string - releaseTime: string -} - -export type SignAndBroadcastPayload = { - bip44Params: Bip44Params - chainId: number - data: string - estimatedFees: FeeDataEstimate - to: string - value: string -} - -export type SignAndBroadcastTx = { - payload: SignAndBroadcastPayload - wallet: ETHWallet - dryRun: boolean - receiverAddress: string | ContractInteraction -} - -export type Signature = { - v: number - r: string - s: string -} - -export type GetTokeRewardAmount = Signature & { - recipient: Recipient -} - -export type TokeClaimIpfs = { - payload: { amount: string; wallet: string; cycle: number; chainId: number } - signature: Signature -} - -export type Recipient = { - chainId: number - cycle: number - wallet: string // address that's claiming. Weird Tokemak naming convention - amount: string -} - -export type StakingContract = { - stakingContract: Contract -} - -// this comment only exists to publish this package - delete me if you see me -export type CanClaimWithdrawParams = { - contractAddress: string - userAddress: string -} - -export type Epoch = { - length: BigInt - number: BigInt - endBlock: BigInt - distribute: BigInt -} diff --git a/src/lib/investor/investor-foxy/api/index.ts b/src/lib/investor/investor-foxy/api/index.ts deleted file mode 100644 index 3318fdbc971..00000000000 --- a/src/lib/investor/investor-foxy/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './api' diff --git a/src/lib/investor/investor-foxy/constants/foxy-addresses.ts b/src/lib/investor/investor-foxy/constants/foxy-addresses.ts deleted file mode 100644 index 158c52337fe..00000000000 --- a/src/lib/investor/investor-foxy/constants/foxy-addresses.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const foxyAddresses = [ - { - staking: '0xee77aa3Fd23BbeBaf94386dD44b548e9a785ea4b', - liquidityReserve: '0x8EC637Fe2800940C7959f9BAd4fE69e41225CD39', - fox: '0xc770EEfAd204B5180dF6a14Ee197D99d808ee52d', - foxy: '0xDc49108ce5C57bc3408c3A5E95F3d864eC386Ed3', - version: 1, - }, -] - -export const tokeRewardHashAddress = '0x5ec3EC6A8aC774c7d53665ebc5DDf89145d02fB6' -export const tokePoolAddress = '0x808D3E6b23516967ceAE4f17a5F9038383ED5311' -export const tokeManagerAddress = '0xa86e412109f77c45a3bc1c5870b880492fb86a14' diff --git a/src/lib/investor/investor-foxy/constants/index.ts b/src/lib/investor/investor-foxy/constants/index.ts deleted file mode 100644 index 8896dd7a133..00000000000 --- a/src/lib/investor/investor-foxy/constants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './foxy-addresses' diff --git a/src/lib/investor/investor-foxy/foxycli.ts b/src/lib/investor/investor-foxy/foxycli.ts deleted file mode 100644 index ce28f544e7b..00000000000 --- a/src/lib/investor/investor-foxy/foxycli.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { ethereum } from '@shapeshiftoss/chain-adapters' -import type { NativeAdapterArgs } from '@shapeshiftoss/hdwallet-native' -import { NativeHDWallet } from '@shapeshiftoss/hdwallet-native' -import { WithdrawType } from '@shapeshiftoss/types' -import * as unchained from '@shapeshiftoss/unchained-client' -import dotenv from 'dotenv' -import { ethers } from 'ethers' -import readline from 'readline-sync' - -import { FoxyApi } from './api' -import { foxyAddresses } from './constants' - -import { bnOrZero } from '@/lib/bignumber/bignumber' - -dotenv.config() - -const { DEVICE_ID = 'device123', MNEMONIC } = process.env - -const getWallet = async (): Promise => { - if (!MNEMONIC) { - throw new Error('Cannot init native wallet without mnemonic') - } - const nativeAdapterArgs: NativeAdapterArgs = { - mnemonic: MNEMONIC, - deviceId: DEVICE_ID, - } - const wallet = new NativeHDWallet(nativeAdapterArgs) - await wallet.initialize() - - return wallet -} - -const main = async (): Promise => { - const wallet = await getWallet() - - const ethChainAdapter = new ethereum.ChainAdapter({ - providers: { - ws: new unchained.ws.Client('wss://dev-api.ethereum.shapeshift.com'), - http: new unchained.ethereum.V1Api( - new unchained.ethereum.Configuration({ - basePath: 'https://dev-api.ethereum.shapeshift.com', - }), - ), - }, - rpcUrl: 'https://dev-daemon.ethereum.shapeshift.com', - thorMidgardUrl: '', - mayaMidgardUrl: '', - }) - - // using 0 value array since only one contract subset exists - const foxyContractAddress = foxyAddresses[0].foxy - const foxContractAddress = foxyAddresses[0].fox - const foxyStakingContractAddress = foxyAddresses[0].staking - const liquidityReserveContractAddress = foxyAddresses[0].liquidityReserve - - const api = new FoxyApi({ - adapter: ethChainAdapter, - providerUrl: import.meta.env.ARCHIVE_NODE || 'http://127.0.0.1:8545/', - foxyAddresses, - provider: new ethers.JsonRpcProvider(import.meta.env.ARCHIVE_NODE || 'http://127.0.0.1:8545/'), - }) - - const accountNumber = 0 - const userAddress = await api.adapter.getAddress({ accountNumber, wallet }) - console.info('current user address ', userAddress) - - const circulatingSupply = async () => { - try { - const supply = await api.tvl({ tokenContractAddress: foxyContractAddress }) - console.info('circulatingSupply', supply.toString()) - } catch (e) { - console.error('Circulating Supply Error:', e) - } - } - - const totalSupply = async () => { - try { - const supply = await api.totalSupply({ tokenContractAddress: foxyContractAddress }) - console.info('totalSupply', supply.toString()) - } catch (e) { - console.error('Total Supply Error:', e) - } - } - - const stakingTokenBalance = async () => { - try { - const balance = await api.balance({ - tokenContractAddress: foxContractAddress, - userAddress, - }) - console.info('Staking Balance', balance.toString()) - } catch (e) { - console.error('Staking Balance Error:', e) - } - } - - const rewardTokenBalance = async () => { - try { - const balance = await api.balance({ - tokenContractAddress: foxyContractAddress, - userAddress, - }) - console.info('Reward Balance', balance.toString()) - } catch (e) { - console.error('Reward Balance Error:', e) - } - } - - const approve = async (tokenContractAddress: string, contractAddress: string) => { - try { - const response = await api.approve({ - tokenContractAddress, - contractAddress, - userAddress, - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('approve', response) - } catch (e) { - console.error('Approve Error:', e) - } - } - - const stake = async (amount: string) => { - try { - console.info('staking...') - const response = await api.deposit({ - contractAddress: foxyStakingContractAddress, - amountDesired: bnOrZero(amount), - userAddress, - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('stake', response) - } catch (e) { - console.error('Stake Error:', e) - } - } - - const unstake = async (amount: string) => { - try { - console.info('unstaking...') - const response = await api.withdraw({ - contractAddress: foxyStakingContractAddress, - amountDesired: bnOrZero(amount), - type: WithdrawType.DELAYED, - userAddress, - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('unstake', response) - } catch (e) { - console.error('Unstake Error:', e) - } - } - - const instantUnstake = async () => { - try { - console.info('instantUnstaking...') - const response = await api.withdraw({ - contractAddress: foxyStakingContractAddress, - type: WithdrawType.INSTANT, - userAddress, - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('instantUnstake', response) - } catch (e) { - console.error('InstantUnstake Error:', e) - } - } - - const claimWithdraw = async (claimAddress: string) => { - try { - console.info('claiming withdraw...') - const response = await api.claimWithdraw({ - contractAddress: foxyStakingContractAddress, - claimAddress, - userAddress, - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('claimWithdraw', response) - } catch (e) { - console.error('ClaimWithdraw Error:', e) - } - } - - const addLiquidity = async (amount: string) => { - try { - console.info('adding liquidity...') - const response = await api.addLiquidity({ - contractAddress: liquidityReserveContractAddress, - userAddress, - amountDesired: bnOrZero(amount), - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('addLiquidity', response) - } catch (e) { - console.error('AddLiquidity Error:', e) - } - } - - const removeLiquidity = async (amount: string) => { - try { - console.info('removing liquidity...') - const response = await api.removeLiquidity({ - contractAddress: liquidityReserveContractAddress, - userAddress, - amountDesired: bnOrZero(amount), - wallet, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('removeLiquidity', response) - } catch (e) { - console.error('RemoveLiquidity Error:', e) - } - } - - const claimToke = async () => { - try { - console.info('Getting claimFromTokemak arguments...') - const response = await api.getClaimFromTokemakArgs({ - contractAddress: foxyStakingContractAddress, - }) - console.info('claimFromTokemak: ', response) - } catch (e) { - console.error('RemoveLiquidity Error:', e) - } - } - - const getTimeUntilClaim = async () => { - try { - console.info('getting time until claim...') - const response = await api.getTimeUntilClaimable({ - contractAddress: foxyStakingContractAddress, - userAddress, - bip44Params: { - accountNumber: 0, - coinType: 60, - purpose: 44, - isChange: false, - addressIndex: 0, - }, - }) - console.info('getTimeUntilClaim', response) - } catch (e) { - console.error('GetTimeUntilClaim Error:', e) - } - } - - const options = [ - 'Approve StakingContract', - 'Approve LiquidityReserve', - 'Stake', - 'Unstake', - 'Instant Unstake', - 'Claim Withdraw', - 'Reward Token Balance', - 'Staking Token Balance', - 'Total Supply', - 'Circulating Supply (TVL)', - 'Cool Down Info', - 'Add Liquidity', - 'Remove Liquidity', - 'Claim From Tokemak', - ] - const contracts = ['Staking Token', 'Reward Token'] - const addresses = ['User Address', 'Liquidity Reserve Address'] - - let index = readline.keyInSelect(options, 'Select an action.\n') - - while (index !== -1) { - let amount = '0' - let tokenContract - let claimAddress - switch (index) { - case 0: - tokenContract = readline.keyInSelect(contracts, 'Which contract do you want to approve.\n') - switch (tokenContract) { - case 0: - await approve(foxContractAddress, foxyStakingContractAddress) - break - case 1: - await approve(foxyContractAddress, foxyStakingContractAddress) - break - default: - break - } - break - case 1: - tokenContract = readline.keyInSelect(contracts, 'Which contract do you want to approve.\n') - switch (tokenContract) { - case 0: - await approve(foxContractAddress, liquidityReserveContractAddress) - break - case 1: - await approve(foxyContractAddress, liquidityReserveContractAddress) - break - default: - break - } - break - case 2: - amount = readline.question('How much do you want to stake?\n') - await stake(amount) - break - case 3: - amount = readline.question('How much do you want to unstake?\n') - await unstake(amount) - break - case 4: - await instantUnstake() - break - case 5: - claimAddress = readline.keyInSelect(addresses, 'Which address do you want to claim.\n') - switch (claimAddress) { - case 0: - await claimWithdraw(userAddress) - break - case 1: - await claimWithdraw(liquidityReserveContractAddress) - break - default: - break - } - break - case 6: - await rewardTokenBalance() - break - case 7: - await stakingTokenBalance() - break - case 8: - await totalSupply() - break - case 9: - await circulatingSupply() - break - case 10: - await getTimeUntilClaim() - break - case 11: - amount = readline.question('How much liqidity do you want to add?\n') - await addLiquidity(amount) - break - case 12: - amount = readline.question('How much liquidity do you want to remove?\n') - await removeLiquidity(amount) - break - case 13: - await claimToke() - break - default: - console.error('invalid action') - } - index = readline.keyInSelect(options, 'Select an action.\n') - } -} - -main().then(() => console.info('Exit')) diff --git a/src/lib/investor/investor-foxy/index.ts b/src/lib/investor/investor-foxy/index.ts deleted file mode 100644 index 608f9555cc8..00000000000 --- a/src/lib/investor/investor-foxy/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './api' -export * from './constants' diff --git a/src/lib/market-service/coingecko/coingecko.ts b/src/lib/market-service/coingecko/coingecko.ts index 4641f2e157f..13fe16ae4c0 100644 --- a/src/lib/market-service/coingecko/coingecko.ts +++ b/src/lib/market-service/coingecko/coingecko.ts @@ -99,8 +99,7 @@ export class CoinGeckoMarketService implements MarketService { } async findByAssetId({ assetId: _assetId }: MarketDataArgs): Promise { - // Monkey patch Arb FOX to mainnet FOX until we have market-data for it, similar to - // what FOXy did in https://github.com/shapeshift/lib/pull/830/files#diff-8d0028d46769c562695ae0eadad8c284637d6a3e45a71a01398c923ae912cf62 + // Monkey patch Arb FOX to mainnet FOX until we have market-data for it const assetId = _assetId === foxOnArbitrumOneAssetId ? foxAssetId : _assetId if (!adapters.assetIdToCoingecko(assetId)) return null diff --git a/src/lib/market-service/foxy/foxy.test.ts b/src/lib/market-service/foxy/foxy.test.ts deleted file mode 100644 index 4a8d3e0ad8d..00000000000 --- a/src/lib/market-service/foxy/foxy.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { HistoryTimeframe } from '@shapeshiftoss/types' -import type { AxiosInstance } from 'axios' -import { ethers } from 'ethers' -import { beforeAll, describe, expect, it, vi } from 'vitest' - -import { FOXY_ASSET_ID, FoxyMarketService } from './foxy' -import { fox, mockFoxyMarketData } from './foxyMockData' - -import { bn } from '@/lib/bignumber/bignumber' - -const foxyMarketService = new FoxyMarketService({ - provider: new ethers.JsonRpcProvider(''), - providerUrls: { - jsonRpcProviderUrl: 'dummy', - unchainedEthereumHttpUrl: '', - unchainedEthereumWsUrl: '', - }, -}) - -const mocks = vi.hoisted(() => ({ - get: vi.fn(), - post: vi.fn(), -})) - -vi.mock('axios', () => { - const mockAxios = { - default: { - create: vi.fn(() => ({ - get: mocks.get, - post: mocks.post, - })), - }, - } - - return { - default: { - ...mockAxios.default.create(), - create: mockAxios.default.create, - }, - } -}) - -vi.mock('axios-cache-interceptor', () => ({ - setupCache: vi.fn().mockImplementation((axiosInstance: AxiosInstance) => axiosInstance), -})) - -const mockTotalSupply = vi.fn().mockReturnValue(bn('502526240759422886301171305')) -const mockTvl = vi.fn().mockReturnValue(bn('52018758965754575223841191')) -vi.mock('@/lib/investor/investor-foxy', () => ({ - FoxyApi: vi.fn().mockImplementation(function () { - return { - totalSupply: mockTotalSupply, - tvl: mockTvl, - } - }), - foxyAddresses: [{ foxy: '0xAddress' }], -})) - -describe('foxy market service', () => { - beforeAll(() => { - vi.clearAllMocks() - }) - - describe('getMarketCap', () => { - it('can return fox market data', async () => { - mocks.get.mockResolvedValue({ data: { data: [{ market_data: fox }] } }) - const result = await foxyMarketService.findAll() - expect(Object.keys(result).length).toEqual(1) - }) - - it('can handle api errors', async () => { - mocks.get.mockRejectedValue({ error: 'foo' }) - const result = await foxyMarketService.findAll() - expect(Object.keys(result).length).toEqual(0) - }) - - it('can handle rate limiting', async () => { - mocks.get.mockRejectedValue({ status: 429 }) - const result = await foxyMarketService.findAll() - expect(Object.keys(result).length).toEqual(0) - }) - }) - - describe('findByAssetId', () => { - const args = { - assetId: FOXY_ASSET_ID, - } - - it('should return market data for FOXy', async () => { - mocks.get.mockResolvedValue({ data: { market_data: fox } }) - expect(await foxyMarketService.findByAssetId(args)).toEqual(mockFoxyMarketData) - }) - - it('should return null on network error', async () => { - mocks.get.mockRejectedValue(Error) - await expect(foxyMarketService.findByAssetId(args)).rejects.toEqual( - new Error('FoxyMarketService(findByAssetId): error fetching market data'), - ) - }) - }) - - describe('findPriceHistoryByAssetId', () => { - const args = { - assetId: FOXY_ASSET_ID, - timeframe: HistoryTimeframe.HOUR, - } - - it('should return market data for FOXy', async () => { - const mockHistoryData = { - prices: [ - [1631664000000, 0.480621954029937], - [1631577600000, 0.48541321175453755], - [1631491200000, 0.4860349080635926], - [1631404800000, 0.46897407484696146], - ], - } - - const expected = [ - { date: new Date('2021-09-15T00:00:00.000Z').valueOf(), price: 0.480621954029937 }, - { date: new Date('2021-09-14T00:00:00.000Z').valueOf(), price: 0.48541321175453755 }, - { date: new Date('2021-09-13T00:00:00.000Z').valueOf(), price: 0.4860349080635926 }, - { date: new Date('2021-09-12T00:00:00.000Z').valueOf(), price: 0.46897407484696146 }, - ] - mocks.get.mockResolvedValue({ data: mockHistoryData }) - expect(await foxyMarketService.findPriceHistoryByAssetId(args)).toEqual(expected) - }) - - it('should return null on network error', async () => { - mocks.get.mockRejectedValue(Error) - await expect(foxyMarketService.findPriceHistoryByAssetId(args)).rejects.toEqual( - new Error('FoxyMarketService(findPriceHistoryByAssetId): error fetching price history'), - ) - }) - }) -}) diff --git a/src/lib/market-service/foxy/foxy.ts b/src/lib/market-service/foxy/foxy.ts deleted file mode 100644 index 9f05bda1a11..00000000000 --- a/src/lib/market-service/foxy/foxy.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { ethereum } from '@shapeshiftoss/chain-adapters' -import type { - HistoryData, - MarketCapResult, - MarketData, - MarketDataArgs, - PriceHistoryArgs, -} from '@shapeshiftoss/types' -import * as unchained from '@shapeshiftoss/unchained-client' -import { BigAmount } from '@shapeshiftoss/utils' -import type { ethers } from 'ethers' - -import type { MarketService } from '../api' -import { CoinGeckoMarketService } from '../coingecko/coingecko' -import type { ProviderUrls } from '../market-service-manager' - -import { foxyAddresses, FoxyApi } from '@/lib/investor/investor-foxy' - -export const FOXY_ASSET_ID = 'eip155:1/erc20:0xDc49108ce5C57bc3408c3A5E95F3d864eC386Ed3' -const FOX_ASSET_ID = 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d' -const FOXY_ASSET_PRECISION = '18' - -export class FoxyMarketService extends CoinGeckoMarketService implements MarketService { - providerUrls: ProviderUrls - provider: ethers.JsonRpcProvider - - constructor({ - providerUrls, - provider, - }: { - providerUrls: ProviderUrls - provider: ethers.JsonRpcProvider - }) { - super() - - this.providerUrls = providerUrls - this.provider = provider - } - - async findAll() { - try { - const assetId = FOXY_ASSET_ID - const marketData = await this.findByAssetId({ assetId }) - - return { [assetId]: marketData } as MarketCapResult - } catch (e) { - console.warn(e) - return {} - } - } - - async findByAssetId({ assetId }: MarketDataArgs): Promise { - try { - if (assetId.toLowerCase() !== FOXY_ASSET_ID.toLowerCase()) return null - - const coinGeckoData = await super.findByAssetId({ - assetId: FOX_ASSET_ID, - }) - - if (!coinGeckoData) return null - - const ethChainAdapter = new ethereum.ChainAdapter({ - providers: { - ws: new unchained.ws.Client( - this.providerUrls.unchainedEthereumWsUrl, - ), - http: new unchained.ethereum.V1Api( - new unchained.ethereum.Configuration({ - basePath: this.providerUrls.unchainedEthereumHttpUrl, - }), - ), - }, - rpcUrl: this.providerUrls.jsonRpcProviderUrl, - thorMidgardUrl: '', - mayaMidgardUrl: '', - }) - - // Make maxSupply as an additional field, effectively EIP-20's totalSupply - const api = new FoxyApi({ - adapter: ethChainAdapter, - providerUrl: this.providerUrls.jsonRpcProviderUrl, - foxyAddresses, - provider: this.provider, - }) - - const tokenContractAddress = foxyAddresses[0].foxy - const foxyTotalSupply = await api.tvl({ tokenContractAddress }) - const supply = foxyTotalSupply - - return { - price: coinGeckoData.price, - marketCap: '0', // TODO: add marketCap once able to get foxy marketCap data - changePercent24Hr: coinGeckoData.changePercent24Hr, - volume: '0', // TODO: add volume once able to get foxy volume data - supply: supply - ? BigAmount.fromBaseUnit({ - value: supply.toFixed(0), - precision: Number(FOXY_ASSET_PRECISION), - }).toPrecision() - : undefined, - maxSupply: foxyTotalSupply - ? BigAmount.fromBaseUnit({ - value: foxyTotalSupply.toFixed(0), - precision: Number(FOXY_ASSET_PRECISION), - }).toPrecision() - : undefined, - } - } catch (e) { - console.warn(e) - throw new Error('FoxyMarketService(findByAssetId): error fetching market data') - } - } - - async findPriceHistoryByAssetId({ - assetId, - timeframe, - }: PriceHistoryArgs): Promise { - if (assetId.toLowerCase() !== FOXY_ASSET_ID.toLowerCase()) return [] - - try { - const priceHistory = await super.findPriceHistoryByAssetId({ - assetId: FOX_ASSET_ID, - timeframe, - }) - return priceHistory - } catch (e) { - console.warn(e) - throw new Error('FoxyMarketService(findPriceHistoryByAssetId): error fetching price history') - } - } -} diff --git a/src/lib/market-service/foxy/foxyMockData.ts b/src/lib/market-service/foxy/foxyMockData.ts deleted file mode 100644 index bd28a68d443..00000000000 --- a/src/lib/market-service/foxy/foxyMockData.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { CoinGeckoMarketData } from '../coingecko/coingecko-types' - -export const fox: CoinGeckoMarketData = { - circulating_supply: 272087306.915483, - max_supply: 1000001337.0, - // This test data should be matching the data coming as Coingecko response. - // The fact that there is loss of precision at runtime doesn't matter. - // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - market_cap: { usd: 76043211.3383411704757409 }, - current_price: { usd: 0.2794809217688426 }, - price_change_percentage_24h: 2.810767605208474, -} - -export const mockFoxyMarketData = { - changePercent24Hr: 2.810767605208474, - marketCap: '0', - price: '0.2794809217688426', - volume: '0', - supply: '52018758.965754575223841191', - maxSupply: '52018758.965754575223841191', -} - -export const mockFoxyPriceHistoryData = [ - { time: 1623110400000, priceUsd: 0.480621954029937 }, - { time: 1623196800000, priceUsd: 0.48541321175453755 }, - { time: 1623283200000, priceUsd: 0.4860349080635926 }, - { time: 1623369600000, priceUsd: 0.46897407484696146 }, - { time: 1623456000000, priceUsd: 0.4569204315609752 }, -] diff --git a/src/lib/market-service/market-service-manager.ts b/src/lib/market-service/market-service-manager.ts index 57715596191..220e81c35aa 100644 --- a/src/lib/market-service/market-service-manager.ts +++ b/src/lib/market-service/market-service-manager.ts @@ -12,7 +12,6 @@ import type { ethers } from 'ethers' import type { MarketService } from './api' import { CoinCapMarketService } from './coincap/coincap' import { CoinGeckoMarketService } from './coingecko/coingecko' -import { FoxyMarketService } from './foxy/foxy' import { PortalsMarketService } from './portals/portals' import { ThorchainAssetsMarketService } from './thorchainAssets/thorchainAssets' import { ZerionMarketService } from './zerion/zerion' @@ -35,16 +34,13 @@ export class MarketServiceManager { marketProviders: MarketService[] assetService: AssetService - constructor(args: MarketServiceManagerArgs) { - const { providerUrls, provider } = args - + constructor(_args: MarketServiceManagerArgs) { this.marketProviders = [ // Order of this MarketProviders array constitutes the order of providers we will be checking first. // More reliable providers should be listed first. new CoinGeckoMarketService(), new CoinCapMarketService(), new PortalsMarketService(), - new FoxyMarketService({ providerUrls, provider }), new ThorchainAssetsMarketService(), new ZerionMarketService(), // TODO: Debank market provider diff --git a/src/lib/market-service/market-service.test.ts b/src/lib/market-service/market-service.test.ts index 4332eb75ed6..7a53b18891f 100644 --- a/src/lib/market-service/market-service.test.ts +++ b/src/lib/market-service/market-service.test.ts @@ -9,7 +9,6 @@ import { mockCGFindByAssetIdData, mockCGPriceHistoryData, } from './coingecko/coingeckoMockData' -import { mockFoxyMarketData, mockFoxyPriceHistoryData } from './foxy/foxyMockData' import { MarketServiceManager } from './market-service-manager' import { mockTcyMarketData, mockTcyPriceHistoryData } from './tcy/tcyMockData' @@ -79,20 +78,6 @@ vi.mock('./zerion/zerion', () => ({ }), })) -const mockFoxyFindAll = vi.fn().mockImplementation(() => mockFoxyMarketData) -const mockFoxyFindByAssetId = vi.fn().mockImplementation(() => mockFoxyMarketData) -const mockFoxyFindPriceHistoryByAssetId = vi.fn().mockImplementation(() => mockFoxyPriceHistoryData) - -vi.mock('./foxy/foxy', () => ({ - FoxyMarketService: vi.fn().mockImplementation(function () { - return { - findAll: mockFoxyFindAll, - findByAssetId: mockFoxyFindByAssetId, - findPriceHistoryByAssetId: mockFoxyFindPriceHistoryByAssetId, - } - }), -})) - const mockThorchainAssetsFindAll = vi.fn().mockImplementation(() => mockTcyMarketData) const mockThorchainAssetsFindByAssetId = vi.fn().mockImplementation(() => mockTcyMarketData) const mockThorchainAssetsFindPriceHistoryByAssetId = vi @@ -149,7 +134,6 @@ describe('market service', () => { mockCoingeckoFindAll.mockRejectedValueOnce({ error: 'error' }) mockCoincapFindAll.mockRejectedValueOnce({ error: 'error' }) mockPortalsFindAll.mockRejectedValueOnce({ error: 'error' }) - mockFoxyFindAll.mockRejectedValueOnce({ error: 'error' }) mockThorchainAssetsFindAll.mockRejectedValueOnce({ error: 'error' }) mockZerionFindAll.mockRejectedValueOnce({ error: 'error' }) await expect(marketServiceManager.findAll({ count: Number() })).rejects.toEqual( @@ -170,7 +154,7 @@ describe('market service', () => { mockZerionFindAll.mockRejectedValueOnce({ error: 'error' }) const marketServiceManager = new MarketServiceManager(marketServiceManagerArgs) const result = await marketServiceManager.findAll({ count: Number() }) - expect(result).toEqual(mockFoxyMarketData) + expect(result).toEqual(mockTcyMarketData) }) }) @@ -191,14 +175,13 @@ describe('market service', () => { mockZerionFindByAssetId.mockRejectedValueOnce({ error: 'error' }) const marketServiceManager = new MarketServiceManager(marketServiceManagerArgs) const result = await marketServiceManager.findByAssetId(ethArgs) - expect(result).toEqual(mockFoxyMarketData) + expect(result).toEqual(mockTcyMarketData) }) it('can return null if no data found', async () => { mockCoingeckoFindByAssetId.mockRejectedValueOnce({ error: 'error' }) mockCoincapFindByAssetId.mockRejectedValueOnce({ error: 'error' }) mockPortalsFindByAssetId.mockRejectedValueOnce({ error: 'error' }) - mockFoxyFindByAssetId.mockRejectedValueOnce({ error: 'error' }) mockThorchainAssetsFindByAssetId.mockRejectedValueOnce({ error: 'error' }) mockZerionFindByAssetId.mockRejectedValueOnce({ error: 'error' }) const marketServiceManager = new MarketServiceManager(marketServiceManagerArgs) @@ -230,14 +213,14 @@ describe('market service', () => { const result = await marketServiceManager.findPriceHistoryByAssetId( findPriceHistoryByAssetIdArgs, ) - expect(result).toEqual(mockFoxyPriceHistoryData) + expect(mockThorchainAssetsFindPriceHistoryByAssetId).toHaveBeenCalled() + expect(result).toEqual([]) }) it('can return null if no data found', async () => { mockCoingeckoFindPriceHistoryByAssetId.mockRejectedValueOnce({ error: 'error' }) mockCoincapFindPriceHistoryByAssetId.mockRejectedValueOnce({ error: 'error' }) mockPortalsFindPriceHistoryByAssetId.mockRejectedValueOnce({ error: 'error' }) - mockFoxyFindPriceHistoryByAssetId.mockRejectedValueOnce({ error: 'error' }) mockThorchainAssetsFindPriceHistoryByAssetId.mockRejectedValueOnce({ error: 'error' }) mockZerionFindPriceHistoryByAssetId.mockRejectedValueOnce({ error: 'error' }) const marketServiceManager = new MarketServiceManager(marketServiceManagerArgs) diff --git a/src/lib/utils/index.test.ts b/src/lib/utils/index.test.ts index d37df01b471..607b3c2e6a5 100644 --- a/src/lib/utils/index.test.ts +++ b/src/lib/utils/index.test.ts @@ -5,7 +5,6 @@ import { ethAssetId, ethChainId, foxAssetId, - foxyAssetId, fromAssetId, } from '@shapeshiftoss/caip' import { contractAddressOrUndefined, isToken } from '@shapeshiftoss/utils' @@ -27,12 +26,7 @@ import { } from '.' import { fauxmesAccountId } from '@/state/slices/opportunitiesSlice/mocks' -import type { - LpId, - OpportunityId, - StakingId, - ValidatorId, -} from '@/state/slices/opportunitiesSlice/types' +import type { LpId, OpportunityId, ValidatorId } from '@/state/slices/opportunitiesSlice/types' import { opportunityIdToChainId } from '@/state/slices/opportunitiesSlice/utils' describe('@/lib/utils', () => { @@ -43,12 +37,6 @@ describe('@/lib/utils', () => { expect(result).toEqual(ethChainId) }) - test('returns the correct chain ID for a StakingId', () => { - const stakingId: StakingId = foxyAssetId as StakingId - const result: ChainId = opportunityIdToChainId(stakingId) - expect(result).toEqual(ethChainId) - }) - test('returns the correct chain ID for a ValidatorId', () => { const validatorId: ValidatorId = fauxmesAccountId as ValidatorId const result: ChainId = opportunityIdToChainId(validatorId) diff --git a/src/pages/Foxy/Foxy.tsx b/src/pages/Foxy/Foxy.tsx new file mode 100644 index 00000000000..6503746c834 --- /dev/null +++ b/src/pages/Foxy/Foxy.tsx @@ -0,0 +1,373 @@ +import { CheckCircleIcon, ExternalLinkIcon } from '@chakra-ui/icons' +import { + Box, + Button, + Card, + CardBody, + Center, + Flex, + Heading, + HStack, + Link, + Stack, + Text, +} from '@chakra-ui/react' +import type { AccountId } from '@shapeshiftoss/caip' +import { ethAssetId, ethChainId, fromAccountId } from '@shapeshiftoss/caip' +import { CONTRACT_INTERACTION } from '@shapeshiftoss/chain-adapters' +import { + ContractType, + FOXY_STAKING_CONTRACT, + getOrCreateContractByType, + viemEthMainnetClient, +} from '@shapeshiftoss/contracts' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import type { Address } from 'viem' +import { encodeFunctionData, formatUnits, getAddress } from 'viem' + +import { Amount } from '@/components/Amount/Amount' +import { Main } from '@/components/Layout/Main' +import { RawText } from '@/components/Text/Text' +import { useWallet } from '@/hooks/useWallet/useWallet' +import { middleEllipsis } from '@/lib/utils' +import { + assertGetEvmChainAdapter, + buildAndBroadcast, + createBuildCustomTxInput, + getApproveContractData, +} from '@/lib/utils/evm' +import { + selectAccountIdsByChainIdFilter, + selectAssetById, + selectPortfolioAccountMetadata, + selectPortfolioLoadingStatus, +} from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' + +const FOXY_TOKEN = '0xDc49108ce5C57bc3408c3A5E95F3d864eC386Ed3' + +// FOXy is a rebasing token, so a fully-recovered position leaves sub-display dust (a few wei of +// gons rounding) rather than an exact 0. Treat anything below this as "nothing to recover" so we +// don't strand a stray "0 FOX" card with an active button after a successful claim. +const DUST_THRESHOLD = 10n ** 14n // 0.0001 FOX + +// Minimal ABI — only what recovery needs. +const stakingAbi = [ + { + type: 'function', + name: 'coolDownInfo', + stateMutability: 'view', + inputs: [{ type: 'address' }], + outputs: [ + { name: 'amount', type: 'uint256' }, + { name: 'gons', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + ], + }, + { + type: 'function', + name: 'unstake', + stateMutability: 'nonpayable', + inputs: [{ type: 'uint256' }, { type: 'bool' }], + outputs: [], + }, + { + type: 'function', + name: 'claimWithdraw', + stateMutability: 'nonpayable', + inputs: [{ type: 'address' }], + outputs: [], + }, +] as const + +type BaseAccount = { accountId: AccountId; accountNumber: number; address: Address } +type FoxyAccount = BaseAccount & { + staked: bigint + allowance: bigint + pending: bigint +} + +export const Foxy = () => { + const wallet = useWallet().state.wallet + const adapter = useMemo(() => assertGetEvmChainAdapter(ethChainId), []) + const foxyToken = useMemo( + () => + getOrCreateContractByType({ + address: FOXY_TOKEN, + type: ContractType.ERC20, + chainId: ethChainId, + }), + [], + ) + + const ethAccountIds = useAppSelector(state => + selectAccountIdsByChainIdFilter(state, { chainId: ethChainId }), + ) + const accountMetadata = useAppSelector(selectPortfolioAccountMetadata) + const isPortfolioLoaded = useAppSelector(selectPortfolioLoadingStatus) === 'success' + + // Every EVM account the connected wallet has, with its derivation index (accountNumber). + const baseAccounts = useMemo( + () => + ethAccountIds + .map(accountId => { + const accountNumber = accountMetadata[accountId]?.bip44Params?.accountNumber + if (accountNumber === undefined) return undefined + return { + accountId, + accountNumber, + address: getAddress(fromAccountId(accountId).account), + } + }) + .filter((a): a is BaseAccount => Boolean(a)), + [ethAccountIds, accountMetadata], + ) + + const [accounts, setAccounts] = useState([]) + const [loaded, setLoaded] = useState(false) + const [loadError, setLoadError] = useState() + const [busyAccountId, setBusyAccountId] = useState() + const [status, setStatus] = useState>({}) + const [recovered, setRecovered] = useState< + Record + >({}) + const reqRef = useRef(0) + + const ethAsset = useAppSelector(state => selectAssetById(state, ethAssetId)) + const txLink = (txHash: string | undefined) => + txHash && ethAsset ? `${ethAsset.explorerTxLink}${txHash}` : undefined + + const refresh = useCallback(async (): Promise => { + const reqId = ++reqRef.current + try { + setLoadError(undefined) + const next = await Promise.all( + baseAccounts.map(async base => { + const [staked, allowance, cd] = await Promise.all([ + foxyToken.read.balanceOf([base.address]), + foxyToken.read.allowance([base.address, getAddress(FOXY_STAKING_CONTRACT)]), + viemEthMainnetClient.readContract({ + address: getAddress(FOXY_STAKING_CONTRACT), + abi: stakingAbi, + functionName: 'coolDownInfo', + args: [base.address], + }), + ]) + return { ...base, staked, allowance, pending: cd[0] } + }), + ) + if (reqId !== reqRef.current) return // a newer refresh superseded this one + setAccounts(next) + } catch (err) { + console.error(err) + if (reqId !== reqRef.current) return + setLoadError('Unable to load FOXy balances. Please try again.') + } finally { + if (reqId === reqRef.current) setLoaded(true) + } + }, [baseAccounts, foxyToken]) + + useEffect(() => { + if (!isPortfolioLoaded) return + refresh() + }, [isPortfolioLoaded, refresh]) + + const send = useCallback( + async (account: BaseAccount, to: string, data: string): Promise => { + if (!wallet) throw new Error('Wallet not connected') + const buildCustomTxInput = await createBuildCustomTxInput({ + accountNumber: account.accountNumber, + from: account.address, + adapter, + data, + to: getAddress(to), + value: '0', + wallet, + }) + const txid = await buildAndBroadcast({ + adapter, + buildCustomTxInput, + receiverAddress: CONTRACT_INTERACTION, + }) + // waitForTransactionReceipt does not throw on a mined-but-reverted tx — surface it so a + // failed approve/unstake shows an error instead of silently continuing the flow. + const receipt = await viemEthMainnetClient.waitForTransactionReceipt({ + hash: txid as `0x${string}`, + }) + if (receipt.status === 'reverted') throw new Error('Transaction reverted') + return txid + }, + [adapter, wallet], + ) + + const recover = useCallback( + async (account: FoxyAccount) => { + const total = account.staked + account.pending + const setMsg = (msg: string) => setStatus(prev => ({ ...prev, [account.accountId]: msg })) + setBusyAccountId(account.accountId) + setMsg('') + try { + // 1. Approve FOXy → staking contract, only if the allowance doesn't already cover it. + if (account.staked > 0n && account.allowance < account.staked) { + setMsg('Approving FOXy…') + await send( + account, + FOXY_TOKEN, + getApproveContractData({ + approvalAmountCryptoBaseUnit: account.staked.toString(), + to: FOXY_TOKEN, + spender: FOXY_STAKING_CONTRACT, + chainId: ethChainId, + }), + ) + } + + // 2. Unstake the full wallet balance. + if (account.staked > 0n) { + setMsg('Unstaking…') + await send( + account, + FOXY_STAKING_CONTRACT, + encodeFunctionData({ + abi: stakingAbi, + functionName: 'unstake', + args: [account.staked, true], + }), + ) + } + + // 3. Claim — sends the FOX to the wallet. + setMsg('Claiming FOX…') + const claimTxHash = await send( + account, + FOXY_STAKING_CONTRACT, + encodeFunctionData({ + abi: stakingAbi, + functionName: 'claimWithdraw', + args: [account.address], + }), + ) + + // Refetch on-chain state so the position reflects the completed recovery. + await refresh() + setRecovered(prev => ({ + ...prev, + [account.accountId]: { amount: total, txHash: claimTxHash }, + })) + setMsg('') + } catch (err) { + console.error(err) + setMsg('Transaction failed or was rejected.') + } finally { + setBusyAccountId(undefined) + } + }, + [refresh, send], + ) + + const displayAccounts = useMemo( + () => + accounts.filter( + a => recovered[a.accountId] !== undefined || a.staked + a.pending >= DUST_THRESHOLD, + ), + [accounts, recovered], + ) + + const body = (() => { + if (!wallet) return Connect a wallet to withdraw your FOX. + if (!isPortfolioLoaded || !loaded) return Loading… + if (loadError) return {loadError} + if (!displayAccounts.length) + return No staked FOX to withdraw for this wallet. + return ( + + {displayAccounts.map(account => { + const recoveredEntry = recovered[account.accountId] + const remaining = account.staked + account.pending + const isRecovered = recoveredEntry !== undefined && remaining < DUST_THRESHOLD + const recoveredTxLink = isRecovered ? txLink(recoveredEntry.txHash) : undefined + const busy = busyAccountId === account.accountId + const error = status[account.accountId] + return ( + + + + + {middleEllipsis(account.address)} + + {isRecovered && ( + + + + Withdrawn + + + )} + + + {recoveredTxLink && ( + + View transaction + + )} + {!isRecovered && ( + + )} + {!isRecovered && !busy && error && ( + + {error} + + )} + + + ) + })} + + ) + })() + + return ( +
+
+ + + + + FOXy + + This staking program has ended. Withdraw your staked FOX below. + + + {body} + + + +
+
+ ) +} diff --git a/src/state/apis/foxy/foxyApi.ts b/src/state/apis/foxy/foxyApi.ts deleted file mode 100644 index 74b7a0db589..00000000000 --- a/src/state/apis/foxy/foxyApi.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { createApi } from '@reduxjs/toolkit/query/react' -import { CHAIN_REFERENCE } from '@shapeshiftoss/caip' -import type { AxiosError } from 'axios' -import axios from 'axios' - -import { BASE_RTK_CREATE_API_CONFIG } from '../const' - -import { getConfig } from '@/config' - -const TOKEMAK_STATS_URL = getConfig().VITE_TOKEMAK_STATS_URL -const TOKEMAK_TFOX_POOL_ADDRESS = '0x808d3e6b23516967ceae4f17a5f9038383ed5311' - -type GetFoxyAprOutput = { - foxyApr: string -} - -type TokemakPool = { - address: string - liquidityProviderApr: string -} - -type TokemakChainData = { - chainId: string - pools: TokemakPool[] -} - -export const foxyApi = createApi({ - ...BASE_RTK_CREATE_API_CONFIG, - reducerPath: 'foxyApi', - endpoints: build => ({ - getFoxyApr: build.query({ - queryFn: async () => { - try { - const response = await axios.get<{ chains: TokemakChainData[] }>(TOKEMAK_STATS_URL) - const tokemakData = response?.data - // Tokemak only supports mainnet for now, so we could just access chains[0], but this keeps things more declarative - const tokemakChainData = tokemakData.chains.find( - ({ chainId }) => chainId === CHAIN_REFERENCE.EthereumMainnet, - ) - - if (!tokemakChainData?.pools) { - return { - error: { - error: 'Cannot get Tokemak pools data', - status: 'CUSTOM_ERROR', - }, - } - } - - const { pools } = tokemakChainData - const tFoxPool = pools.find(({ address }) => address === TOKEMAK_TFOX_POOL_ADDRESS) - - if (!tFoxPool) { - return { - error: { - error: 'Cannot get Tokemak TFOX pool data', - status: 'CUSTOM_ERROR', - }, - } - } - - return { data: { foxyApr: tFoxPool.liquidityProviderApr } } - } catch (e) { - if ((e as AxiosError).isAxiosError) { - return { - error: { - error: - (e as AxiosError).response?.statusText ?? 'Cannot get Tokemak TFOX pool data', - status: (e as AxiosError).response?.status ?? 400, - }, - } - } - - return { - error: { - error: 'Cannot get Tokemak TFOX pool data', - status: 'PARSING_ERROR', - }, - } - } - }, - }), - }), -}) - -export const { useGetFoxyAprQuery } = foxyApi diff --git a/src/state/apis/foxy/foxyApiSingleton.ts b/src/state/apis/foxy/foxyApiSingleton.ts deleted file mode 100644 index c0ced8d8591..00000000000 --- a/src/state/apis/foxy/foxyApiSingleton.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { EvmBaseAdapter } from '@shapeshiftoss/chain-adapters' -import { getEthersProvider } from '@shapeshiftoss/contracts' -import { KnownChainIds } from '@shapeshiftoss/types' - -import { getConfig } from '@/config' -import { foxyAddresses, FoxyApi } from '@/lib/investor/investor-foxy' -import { assertGetEvmChainAdapter } from '@/lib/utils/evm' - -// don't export me, access me through the getter -let _foxyApi: FoxyApi | undefined = undefined - -// we need to be able to access this outside react -export const getFoxyApi = (): FoxyApi => { - const RPC_PROVIDER_ENV = 'VITE_ETHEREUM_NODE_URL' - - if (_foxyApi) return _foxyApi - - const foxyApi = new FoxyApi({ - adapter: assertGetEvmChainAdapter( - KnownChainIds.EthereumMainnet, - ) as EvmBaseAdapter, - providerUrl: getConfig()[RPC_PROVIDER_ENV], - foxyAddresses, - provider: getEthersProvider(KnownChainIds.EthereumMainnet), - }) - - _foxyApi = foxyApi - - return _foxyApi -} diff --git a/src/state/reducer.ts b/src/state/reducer.ts index 23cddf06680..794d87960a4 100644 --- a/src/state/reducer.ts +++ b/src/state/reducer.ts @@ -3,7 +3,6 @@ import { createMigrate, persistReducer } from 'redux-persist' import { abiApi } from './apis/abi/abiApi' import { fiatRampApi } from './apis/fiatRamps/fiatRamps' -import { foxyApi } from './apis/foxy/foxyApi' import { limitOrderApi } from './apis/limit-orders/limitOrderApi' import { portalsApi } from './apis/portals/portalsApi' import type { SnapshotState } from './apis/snapshot/snapshot' @@ -232,7 +231,6 @@ export const apiSlices = { marketApi, txHistoryApi, swappersApi: swapperApi, - foxyApi, fiatRampApi, snapshotApi, portalsApi, @@ -247,7 +245,6 @@ export const apiReducers = { [marketApi.reducerPath]: marketApi.reducer, [txHistoryApi.reducerPath]: txHistoryApi.reducer, [swapperApi.reducerPath]: swapperApi.reducer, - [foxyApi.reducerPath]: foxyApi.reducer, [fiatRampApi.reducerPath]: fiatRampApi.reducer, [snapshotApi.reducerPath]: snapshotApi.reducer, [portalsApi.reducerPath]: portalsApi.reducer, diff --git a/src/state/slices/opportunitiesSlice/constants.ts b/src/state/slices/opportunitiesSlice/constants.ts index 645c4f7502f..4572119ee82 100644 --- a/src/state/slices/opportunitiesSlice/constants.ts +++ b/src/state/slices/opportunitiesSlice/constants.ts @@ -85,12 +85,6 @@ export const rFOXStakingIds = RFOX_STAKING_ASSET_IDS as readonly StakingId[] export const STAKING_ID_DELIMITER = '*' export const DEFI_PROVIDER_TO_METADATA: Record = { - [DefiProvider.ShapeShift]: { - provider: DefiProvider.ShapeShift, - icon: '/fox-token-logo.png', - color: '#3761F9', - url: 'https://app.shapeshift.com', - }, [DefiProvider.EthFoxStaking]: { provider: DefiProvider.EthFoxStaking, icon: '/fox-token-logo.png', diff --git a/src/state/slices/opportunitiesSlice/mappings.ts b/src/state/slices/opportunitiesSlice/mappings.ts index 5c02c9ef1c3..198cd76b6d1 100644 --- a/src/state/slices/opportunitiesSlice/mappings.ts +++ b/src/state/slices/opportunitiesSlice/mappings.ts @@ -11,11 +11,6 @@ import { ethFoxStakingOpportunityIdsResolver, ethFoxStakingUserDataResolver, } from './resolvers/ethFoxStaking' -import { - foxyStakingOpportunitiesMetadataResolver, - foxyStakingOpportunitiesUserDataResolver, - foxyStakingOpportunityIdsResolver, -} from './resolvers/foxy' import { rFOXStakingMetadataResolver, rFOXStakingOpportunityIdsResolver, @@ -52,16 +47,10 @@ export const DefiProviderToOpportunitiesMetadataResolverByDeFiType: DefiProvider [`${DefiProvider.ThorchainSavers}`]: { [`${DefiType.Staking}`]: thorchainSaversStakingOpportunitiesMetadataResolver, }, - [`${DefiProvider.ShapeShift}`]: { - [`${DefiType.Staking}`]: foxyStakingOpportunitiesMetadataResolver, - }, } export const DefiProviderToOpportunitiesUserDataResolverByDeFiType: DefiProviderToOpportunitiesUserDataResolver = { - [`${DefiProvider.ShapeShift}`]: { - [`${DefiType.Staking}`]: foxyStakingOpportunitiesUserDataResolver, - }, [`${DefiProvider.ThorchainSavers}`]: { [`${DefiType.Staking}`]: thorchainSaversStakingOpportunitiesUserDataResolver, }, @@ -81,9 +70,6 @@ export const DefiProviderToOpportunityIdsResolverByDeFiType: DefiProviderToOppor [`${DefiProvider.ThorchainSavers}`]: { [`${DefiType.Staking}`]: thorchainSaversOpportunityIdsResolver, }, - [`${DefiProvider.ShapeShift}`]: { - [`${DefiType.Staking}`]: foxyStakingOpportunityIdsResolver, - }, [`${DefiProvider.CosmosSdk}`]: { [`${DefiType.Staking}`]: cosmosSdkOpportunityIdsResolver, }, @@ -144,10 +130,6 @@ export const CHAIN_ID_TO_SUPPORTED_DEFI_OPPORTUNITIES: Record< defiProvider: DefiProvider.ThorchainSavers, defiType: DefiType.Staking, }, - { - defiProvider: DefiProvider.ShapeShift, - defiType: DefiType.Staking, - }, ], [KnownChainIds.BnbSmartChainMainnet]: [ { diff --git a/src/state/slices/opportunitiesSlice/resolvers/foxy/index.ts b/src/state/slices/opportunitiesSlice/resolvers/foxy/index.ts deleted file mode 100644 index c2ee5aa2403..00000000000 --- a/src/state/slices/opportunitiesSlice/resolvers/foxy/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -import type { ToAssetIdArgs } from '@shapeshiftoss/caip' -import { ethChainId, foxyAssetId, fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' -import { BigAmount } from '@shapeshiftoss/utils' -import dayjs from 'dayjs' - -import type { - GetOpportunityIdsOutput, - GetOpportunityMetadataOutput, - GetOpportunityUserStakingDataOutput, - OpportunitiesState, - OpportunityMetadata, - StakingId, -} from '../../types' -import { DefiProvider, DefiType } from '../../types' -import { serializeUserStakingId, toOpportunityId } from '../../utils' -import type { - OpportunitiesMetadataResolverInput, - OpportunitiesUserDataResolverInput, -} from '../types' - -import { bnOrZero } from '@/lib/bignumber/bignumber' -import { foxyApi } from '@/state/apis/foxy/foxyApi' -import { getFoxyApi } from '@/state/apis/foxy/foxyApiSingleton' -import { selectAssetById } from '@/state/slices/assetsSlice/selectors' -import { selectPortfolioCryptoBalanceByFilter } from '@/state/slices/common-selectors' -import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/marketDataSlice/selectors' -import { selectBip44ParamsByAccountId } from '@/state/slices/portfolioSlice/selectors' - -export const foxyStakingOpportunitiesMetadataResolver = async ({ - defiType, - reduxApi, -}: OpportunitiesMetadataResolverInput): Promise<{ data: GetOpportunityMetadataOutput }> => { - const allOpportunities = await getFoxyApi().getFoxyOpportunities() - - const foxyApr = await reduxApi.dispatch(foxyApi.endpoints.getFoxyApr.initiate()) - - const { getState } = reduxApi - const state: any = getState() // ReduxState causes circular dependency - - const stakingOpportunitiesById: Record = {} - - for (const opportunity of allOpportunities) { - // FOXY Token - const rewardTokenAssetId = toAssetId({ - chainId: ethChainId, - assetNamespace: 'erc20', - assetReference: opportunity.rewardToken, - }) - // FOX Token - const tokenAssetId = toAssetId({ - chainId: ethChainId, - assetNamespace: 'erc20', - assetReference: opportunity.stakingToken, - }) - // FOXy staking contract - const toAssetIdParts: ToAssetIdArgs = { - assetNamespace: 'erc20', - assetReference: opportunity.contractAddress, - chainId: ethChainId, - } - - const assetId = toAssetId(toAssetIdParts) - const opportunityId = toOpportunityId(toAssetIdParts) - const underlyingAsset = selectAssetById(state, tokenAssetId) - const marketData = selectMarketDataByAssetIdUserCurrency(state, tokenAssetId) - - if (!underlyingAsset) continue - - const tvl = BigAmount.fromBaseUnit({ - value: opportunity.tvl?.toString() ?? '0', - precision: underlyingAsset?.precision ?? 0, - }) - .times(marketData?.price ?? '0') - .toPrecision() - - const apy = foxyApr.data?.foxyApr ?? '0' - - stakingOpportunitiesById[opportunityId] = { - apy, - assetId, - id: opportunityId, - provider: DefiProvider.ShapeShift as const, - tvl, - type: DefiType.Staking as const, - underlyingAssetId: rewardTokenAssetId, - underlyingAssetIds: [tokenAssetId], - underlyingAssetRatiosBaseUnit: [ - BigAmount.fromPrecision({ value: '1', precision: underlyingAsset.precision }).toBaseUnit(), - ], - name: underlyingAsset.symbol, - rewardAssetIds: [], - isClaimableRewards: true, - expired: true, - } - } - - const data = { - byId: stakingOpportunitiesById, - type: defiType, - } - - return { data } -} - -export const foxyStakingOpportunitiesUserDataResolver = async ({ - accountId, - reduxApi, - opportunityIds, -}: OpportunitiesUserDataResolverInput): Promise<{ data: GetOpportunityUserStakingDataOutput }> => { - const { getState } = reduxApi - const state: any = getState() // ReduxState causes circular dependency - - const stakingOpportunitiesUserDataByUserStakingId: OpportunitiesState['userStaking']['byId'] = {} - - const foxyInvestor = getFoxyApi() - - for (const stakingOpportunityId of opportunityIds) { - const balanceFilter = { accountId, assetId: foxyAssetId } - const balance = selectPortfolioCryptoBalanceByFilter(state, balanceFilter) - - const asset = selectAssetById(state, foxyAssetId) - if (!asset) continue - - const toAssetIdParts: ToAssetIdArgs = { - assetNamespace: fromAssetId(stakingOpportunityId).assetNamespace, - assetReference: fromAssetId(stakingOpportunityId).assetReference, - chainId: fromAssetId(stakingOpportunityId).chainId, - } - const opportunityId = toOpportunityId(toAssetIdParts) - const userStakingId = serializeUserStakingId(accountId, opportunityId) - - const opportunities = await foxyInvestor.getFoxyOpportunities() - - // investor-foxy is architected around many FOXy addresses/opportunity, but akchually there's only one - if (!opportunities[0]) continue - - const opportunity = opportunities[0] - - // FOXy is a rebasing token so there aren't rewards in the sense of rewards claim - // These technically exist and are effectively accrued, but we're unable to derive them - const rewardsAmountsCryptoBaseUnit = ['0'] as [string] | [string, string] - - const bip44Params = selectBip44ParamsByAccountId(state, { accountId }) - - if (!bip44Params) continue - - const withdrawInfo = await foxyInvestor.getWithdrawInfo({ - contractAddress: opportunity.contractAddress, - userAddress: fromAccountId(accountId).account, - bip44Params, - }) - - const undelegations = [ - { - completionTime: dayjs(withdrawInfo.releaseTime).unix(), - undelegationAmountCryptoBaseUnit: bnOrZero(withdrawInfo.amount).toFixed(), - }, - ] - - stakingOpportunitiesUserDataByUserStakingId[userStakingId] = { - isLoaded: true, - userStakingId, - stakedAmountCryptoBaseUnit: balance.toBaseUnit(), - rewardsCryptoBaseUnit: { amounts: rewardsAmountsCryptoBaseUnit, claimable: true }, - undelegations, - } - } - - const data = { - byId: stakingOpportunitiesUserDataByUserStakingId, - } - - return Promise.resolve({ data }) -} - -export const foxyStakingOpportunityIdsResolver = async (): Promise<{ - data: GetOpportunityIdsOutput -}> => { - const opportunities = await getFoxyApi().getFoxyOpportunities() - - return { - data: opportunities.map(opportunity => { - const assetId = toOpportunityId({ - assetNamespace: 'erc20', - assetReference: opportunity.contractAddress, - chainId: ethChainId, - }) - return assetId - }), - } -} diff --git a/src/state/slices/opportunitiesSlice/resolvers/foxy/types.ts b/src/state/slices/opportunitiesSlice/resolvers/foxy/types.ts deleted file mode 100644 index 29e8a2acdb5..00000000000 --- a/src/state/slices/opportunitiesSlice/resolvers/foxy/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UserStakingOpportunityBase } from '../../types' - -export type UserUndelegation = { - completionTime: number - undelegationAmountCryptoBaseUnit: string -} - -export type FoxySpecificUserStakingOpportunity = UserStakingOpportunityBase & { - // Undelegations is a Cosmos SDK specific terminology https://docs.cosmos.network/main/modules/staking - // The terminology has been reused here for FOXy to keep things abstracted, but Cosmos SDK undelegations - // and FOXy delayed withdraws are two very different implementations, on two different chains - undelegations: UserUndelegation[] -} diff --git a/src/state/slices/opportunitiesSlice/types.ts b/src/state/slices/opportunitiesSlice/types.ts index 92f23f3bebb..3f5219d48fe 100644 --- a/src/state/slices/opportunitiesSlice/types.ts +++ b/src/state/slices/opportunitiesSlice/types.ts @@ -2,7 +2,6 @@ import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import type { PartialRecord } from '@shapeshiftoss/types' import type { CosmosSdkStakingSpecificUserStakingOpportunity } from './resolvers/cosmosSdk/types' -import type { FoxySpecificUserStakingOpportunity } from './resolvers/foxy/types' import type { ThorchainSaversStakingSpecificMetadata } from './resolvers/thorchainsavers/types' import type { OpportunitiesMetadataResolverInput, @@ -20,7 +19,6 @@ export enum DefiType { } export enum DefiProvider { - ShapeShift = 'ShapeShift', rFOX = 'rFOX', EthFoxStaking = 'ETH/FOX Staking', CosmosSdk = 'Cosmos SDK', @@ -63,7 +61,7 @@ export type OpportunityMetadataBase = { underlyingAssetId: AssetId // The AssetId or AssetIds this opportunity represents // For LP tokens, that's an asset pair - // For opportunities a la FOXy, that's the asset the opportunity wraps + // For other opportunities, that is the asset the opportunity wraps underlyingAssetIds: AssetId[] // The underlying amount of underlyingAssetId per 1 LP token, in base unit underlyingAssetRatiosBaseUnit: readonly string[] @@ -109,7 +107,6 @@ export type UserStakingOpportunity = | UserStakingOpportunityBase | SaversUserStakingOpportunity | CosmosSdkStakingSpecificUserStakingOpportunity - | FoxySpecificUserStakingOpportunity export type UserStakingOpportunityWithMetadata = UserStakingOpportunity & OpportunityMetadata diff --git a/src/state/slices/opportunitiesSlice/utils/index.ts b/src/state/slices/opportunitiesSlice/utils/index.ts index f547bba1379..1c44654b8a0 100644 --- a/src/state/slices/opportunitiesSlice/utils/index.ts +++ b/src/state/slices/opportunitiesSlice/utils/index.ts @@ -8,7 +8,6 @@ import type { CosmosSdkStakingSpecificUserStakingOpportunity, UserUndelegation, } from '../resolvers/cosmosSdk/types' -import type { FoxySpecificUserStakingOpportunity } from '../resolvers/foxy/types' import type { OpportunityId, OpportunityMetadataBase, @@ -132,9 +131,8 @@ export const toValidatorId = (...[args]: Parameters) => export const supportsUndelegations = ( userStakingOpportunity: Partial, -): userStakingOpportunity is - | CosmosSdkStakingSpecificUserStakingOpportunity - | FoxySpecificUserStakingOpportunity => 'undelegations' in userStakingOpportunity +): userStakingOpportunity is CosmosSdkStakingSpecificUserStakingOpportunity => + 'undelegations' in userStakingOpportunity export const makeTotalUndelegationsCryptoBaseUnit = (undelegations: UserUndelegation[]) => undelegations.reduce((a, { undelegationAmountCryptoBaseUnit: b }) => a.plus(b), bn(0)) diff --git a/src/state/store.ts b/src/state/store.ts index e36682a6079..01dacf26f89 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -8,7 +8,6 @@ import { setGlobalDevModeChecks } from 'reselect' import { abiApi } from './apis/abi/abiApi' import { fiatRampApi } from './apis/fiatRamps/fiatRamps' -import { foxyApi } from './apis/foxy/foxyApi' import { limitOrderApi } from './apis/limit-orders/limitOrderApi' import { portalsApi } from './apis/portals/portalsApi' import { snapshotApi } from './apis/snapshot/snapshot' @@ -39,7 +38,6 @@ const apiMiddleware = [ marketApi.middleware, assetApi.middleware, txHistoryApi.middleware, - foxyApi.middleware, swapperApi.middleware, fiatRampApi.middleware, snapshotApi.middleware, diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 1f522362b44..783e0d0ee34 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -60,7 +60,6 @@ export const mockStore: ReduxState = { txHistoryApi: mockApiFactory('txHistoryApi' as const), portalsApi: mockApiFactory('portalsApi' as const), swapperApi: mockSwapperApi, - foxyApi: mockApiFactory('foxyApi' as const), fiatRampApi: mockApiFactory('fiatRampApi' as const), snapshotApi: mockApiFactory('snapshotApi' as const), opportunitiesApi: mockApiFactory('opportunitiesApi' as const),