diff --git a/.changeset/hot-balloons-wave.md b/.changeset/hot-balloons-wave.md deleted file mode 100644 index 90a1b2cf4d..0000000000 --- a/.changeset/hot-balloons-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@coinbase/onchainkit': patch ---- - -- **feat**: convert TokeSearch to css and add modifier styles. By @kyhyco #460 diff --git a/.changeset/thin-apples-grow.md b/.changeset/thin-apples-grow.md deleted file mode 100644 index a296553a8b..0000000000 --- a/.changeset/thin-apples-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@coinbase/onchainkit": patch ---- - -- **docs**: add contribution guide. By @kyhyco #459 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 558ad567f8..4756dbb2c9 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -2,7 +2,7 @@ name: Setup runs: using: composite steps: - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' registry-url: https://registry.npmjs.org diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7c7facc3c..46c699dc75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,14 +12,18 @@ jobs: node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' + - name: Kit Install dependencies run: npm install + - name: Kit Test Build # When fails, please check your build run: | diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 381adb373f..9d2a302bf1 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 719b535190..585b119d18 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,13 +12,17 @@ jobs: node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' + - name: Kit Install dependencies run: npm install + - name: Kit Format Check run: npm run format:check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f24c118cc8..58f5319ddc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/salus-scan.yml b/.github/workflows/salus-scan.yml index 272bf90f46..5975f127a4 100644 --- a/.github/workflows/salus-scan.yml +++ b/.github/workflows/salus-scan.yml @@ -6,7 +6,7 @@ jobs: scan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Salus Scan id: salus_scan uses: federacy/scan-action@0.1.4 diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 7c1a9ae7f7..32b6d232cb 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -12,15 +12,19 @@ jobs: node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' + - name: Site Install dependencies working-directory: ./site run: npm install + - name: Site Test Build working-directory: ./site run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 881f067d30..6e21047faf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,14 +12,18 @@ jobs: node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' + - name: Kit Install dependencies run: npm install && npm install --prefix ./framegear + - name: Kit Test Check # When fails, please check your tests run: npm run test:coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index afeb4072c4..ff92bcb957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,83 @@ # Changelog +## 0.19.7 + +### Patch Changes + +- a47b07f: - **fix**: have API_KEY set correctly by `OnchainKitProvider` and avoid request CORS issue with the `onchainkit_version` Header. By @zizzamia #501 + +## 0.19.6 + +### Patch Changes + +- c54ec7b: - **fix**: added border transparent to Badge component. By @zizzamia #493 + +## 0.19.5 + +### Patch Changes + +- **chore**: continue migrating to Tailwind for CSS internals. By @zizzamia #492 477e1f5 +- **fix**: reduce gap between `TokenSelector` and `TokenSelectorDropdown`. By @kyhyco #489 +- **docs**: add back `TokenSelectorDropdown` example. By @kyhyco #487 + +## 0.19.4 + +### Patch Changes + +- **feat**: updated TokenKit styles. By @kyhyco #482 10d1fa9 + +## 0.19.3 + +### Patch Changes + +- **feat**: added `getQuote`. by @0xAlec #479 fdab188 +- **feat**: deprecated `LegacyTokenData`. By @0xAlec #478 +- **feat**: added `TokenSelectorDropdown` to use with `TokenSelector`. By @kyhyco #475 + +## 0.19.2 + +### Patch Changes + +- **feat**: converted `Badge` to css. By @zizzamia #476 9b03393 +- **feat**: converted `TokenRow` to css, add modifier state and add additinal display controls. By @kyhyco #473 + +## 0.19.1 + +### Patch Changes + +- **feat**: for `getAvatar` now we use `ensName` instead of `name`. By @zizzamia #471 b6653f1 + +## 0.19.0 + +### Minor Changes + +- **feat**: standardized `getAvatar()`. By @roushou #464 029ba7d + +- **feat**: `TokenImage` with no image renders partial token symbol and deterministic dark color. By @kyhyco#468 +- **feat**: converted `TokeSearch` to css and add modifier styles. By @kyhyco #460 +- **docs**: added contribution guide. By @kyhyco #459 + +Breaking changes + +- Changed the definition of `getAvatar(...)`, from `getAvatar(ensName: string)` to `getAvatar(params: {ensName: string })`. +- Changed `TokenImage` props from + +```ts +export type TokenImageReact = { + src: string | null; + size?: number; +}; +``` + +to + +```ts +export type TokenImageReact = { + token: Token; + size?: number; +}; +``` + ## 0.18.6 ### Patch Changes diff --git a/README.md b/README.md index 9e2126be95..cac7830527 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- - OnchainKit logo vibes + + OnchainKit logo vibes

@@ -61,6 +61,17 @@ npm install @coinbase/onchainkit viem@2.x # Use PNPM pnpm add @coinbase/onchainkit viem@2.x + +# Use BUN +bun add @coinbase/onchainkit viem@2.x +``` + +## CSS + +Add this at the top of your application entry point + +```javascript +import '@coinbase/onchainkit/styles.css'; ``` ## Components diff --git a/biome.json b/biome.json new file mode 100644 index 0000000000..b076306e9c --- /dev/null +++ b/biome.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentWidth": 2, + "indentStyle": "space" + }, + "javascript": { + "formatter": { + "enabled": true, + "lineWidth": 100, + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "indentWidth": 2, + "indentStyle": "space", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "nursery": { + "useSortedClasses": "error" + } + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + }, + "files": { + "ignore": [ + ".yarn", + ".changeset", + ".next", + "coverage", + "esm", + "site", + "framegear", + "src/frame/", + "src/identity/", + "src/queries/", + "src/token/", + "src/utils/", + "src/wallet/", + "src/xmtp" + ] + } +} diff --git a/jest.setup.ts b/jest.setup.ts index 66b3d247e3..f5cc4b23ed 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -5,7 +5,7 @@ import 'jest-extended'; // Enable jest-dom functions import '@testing-library/jest-dom'; - +// biome-ignore lint: Running linting with `unsafe` flag changes import to 'node:util' which breaks the CI. See https://nodejs.org/docs/latest-v18.x/api/util.html import { TextEncoder, TextDecoder } from 'util'; global.TextEncoder = TextEncoder; diff --git a/package.json b/package.json index ed38a1b6fb..50c45840ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/onchainkit", - "version": "0.18.6", + "version": "0.19.7", "type": "module", "repository": "https://github.com/coinbase/onchainkit.git", "license": "MIT", @@ -10,6 +10,9 @@ "check": "yarn format", "format": "prettier --log-level warn --write .", "format:check": "prettier --check .", + "format:biome": "biome format --write .", + "lint": "biome lint --write .", + "lint:unsafe": "biome lint --write --unsafe .", "test": "jest --testPathIgnorePatterns=\\.integ\\.", "test:integration": "jest --testPathIgnorePatterns=\\.test\\.", "test:all": "jest .", @@ -31,6 +34,7 @@ "wagmi": "^2" }, "devDependencies": { + "@biomejs/biome": "^1.8.0", "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.2", "@coinbase/wallet-sdk": "^4.0.0", @@ -142,5 +146,6 @@ "import": "./esm/wallet/index.js", "default": "./lib/wallet/index.js" } - } + }, + "packageManager": "yarn@4.0.2" } diff --git a/site/.env.d.ts b/site/.env.d.ts new file mode 100644 index 0000000000..eb4af09ed7 --- /dev/null +++ b/site/.env.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface ProcessEnv { + NEXT_PUBLIC_GOOGLE_ANALYTICS_ID: string; + } +} diff --git a/site/docs/components/TokenSelectorContainer.tsx b/site/docs/components/TokenSelectorContainer.tsx new file mode 100644 index 0000000000..f752f7b022 --- /dev/null +++ b/site/docs/components/TokenSelectorContainer.tsx @@ -0,0 +1,12 @@ +import { ReactElement, useState } from 'react'; +import type { Token } from '@coinbase/onchainkit/token'; + +type TokenSelectorContainer = { + children: (token: Token, setToken: (t: Token) => void) => ReactElement; +}; + +export default function TokenSelectorContainer({ children }: TokenSelectorContainer) { + const [token, setToken] = useState(); + + return children(token, setToken); +} diff --git a/site/docs/pages/is-base.mdx b/site/docs/pages/config/is-base.mdx similarity index 91% rename from site/docs/pages/is-base.mdx rename to site/docs/pages/config/is-base.mdx index 0f838b5a5e..b9dd7881d8 100644 --- a/site/docs/pages/is-base.mdx +++ b/site/docs/pages/config/is-base.mdx @@ -30,4 +30,4 @@ true; ## Parameters -[`isBaseOptions`](/types#isbaseoptions) +[`isBaseOptions`](/config/types#isbaseoptions) diff --git a/site/docs/pages/config/onchainkit-provider.mdx b/site/docs/pages/config/onchainkit-provider.mdx new file mode 100644 index 0000000000..95ae7d1676 --- /dev/null +++ b/site/docs/pages/config/onchainkit-provider.mdx @@ -0,0 +1,22 @@ +# `` + +Provides the OnchainKit React Context to the app. + +## Usage + +```tsx [code] +import { base } from 'viem/chains'; +import { OnchainKitProvider } from '@coinbase/onchainkit'; + +const App = () => { + return ( + + + + ); +}; +``` + +## Props + +[`OnchainKitProviderReact`](/config/types#onchainkitproviderreact) diff --git a/site/docs/pages/types.mdx b/site/docs/pages/config/types.mdx similarity index 100% rename from site/docs/pages/types.mdx rename to site/docs/pages/config/types.mdx diff --git a/site/docs/pages/frame/framegear.mdx b/site/docs/pages/frame/framegear.mdx index c639987ee9..26822fbbc3 100644 --- a/site/docs/pages/frame/framegear.mdx +++ b/site/docs/pages/frame/framegear.mdx @@ -36,6 +36,14 @@ pnpm install pnpm run dev ``` +```bash [bun] +git clone https://github.com/coinbase/onchainkit.git + +cd onchainkit/framegear +bun install +bun run dev +``` + ::: Visit http://localhost:1337 to start the **Framegear** interface. Enter the URL of your locally diff --git a/site/docs/pages/frame/introduction.mdx b/site/docs/pages/frame/introduction.mdx index 91297ceceb..9d4ed330d9 100644 --- a/site/docs/pages/frame/introduction.mdx +++ b/site/docs/pages/frame/introduction.mdx @@ -31,3 +31,9 @@ yarn add @coinbase/onchainkit react@18 react-dom@18 ```bash [pnpm] pnpm add @coinbase/onchainkit react@18 react-dom@18 ``` + +```bash [bun] +bun add @coinbase/onchainkit react@18 react-dom@18 +``` + +::: diff --git a/site/docs/pages/getting-started.mdx b/site/docs/pages/getting-started.mdx index 8caef858e7..33c6990e24 100644 --- a/site/docs/pages/getting-started.mdx +++ b/site/docs/pages/getting-started.mdx @@ -1,8 +1,12 @@ # Getting Started -Add OnchainKit to your project, install the required packages. +You can use OnchainKit in an existing project, by installing OnchainKit as a dependency. -## Installation +::::steps + +### Install + +Let's install OnchainKit as a dependency along with its Viem peer dependency. :::code-group @@ -18,8 +22,51 @@ yarn add @coinbase/onchainkit viem@2.x pnpm add @coinbase/onchainkit viem@2.x ``` +```bash [bun] +bun add @coinbase/onchainkit viem@2.x +``` + ::: +### Configure the OnchainKitProvider + +The `` component equips your app with the essential context to interact with OnchainKit components and utilities. + +Set the `chain` prop to your target chain and provide the API KEY to access the embedded APIs. + +```tsx [code] +import { base } from 'viem/chains'; +import { OnchainKitProvider } from '@coinbase/onchainkit'; + +const App = () => { + return ( + + + + ); +}; +``` + +Obtain an API key from the [Coinbase Developer Platform APIs](https://www.coinbase.com/developer-platform). + +OnchainKit copy API KEY + +### Style your components + +All OnchainKit components come pre-configured with a style. + +Simply place this at the top of your application's entry point to have the components working out of the box. + +```javascript +import '@coinbase/onchainkit/styles.css'; +``` + +:::: + OnchainKit is divided into various theme utilities and components that are available for your use: - [Frame](/frame/introduction) diff --git a/site/docs/pages/contribution-guide.mdx b/site/docs/pages/guides/contribution.mdx similarity index 98% rename from site/docs/pages/contribution-guide.mdx rename to site/docs/pages/guides/contribution.mdx index 600da36a8b..c8ec846829 100644 --- a/site/docs/pages/contribution-guide.mdx +++ b/site/docs/pages/guides/contribution.mdx @@ -5,7 +5,7 @@ description: Learn how to contribute to OnchainKit # OnchainKit Contribution Guide -Welcome to OnchainKit! So you want to contribute to this project? You can to the right place. +Welcome to OnchainKit! So you want to contribute to this project? You came to the right place. In this guide, you will learn how to: diff --git a/site/docs/pages/identity/avatar.mdx b/site/docs/pages/identity/avatar.mdx index c45dc06789..b762e58b18 100644 --- a/site/docs/pages/identity/avatar.mdx +++ b/site/docs/pages/identity/avatar.mdx @@ -21,12 +21,13 @@ import { Avatar } from '@coinbase/onchainkit/identity'; ```html [return html] zizzamia.eth ``` @@ -60,3 +61,11 @@ import { Avatar } from '@coinbase/onchainkit/identity'; ## Props [`AvatarReact`](/identity/types#AvatarReact) + +## CSS + +```css +.ock-avatar { + @apply rounded-[50%]; +} +``` diff --git a/site/docs/pages/identity/badge.mdx b/site/docs/pages/identity/badge.mdx index 84181ed563..e0bb21b6dd 100644 --- a/site/docs/pages/identity/badge.mdx +++ b/site/docs/pages/identity/badge.mdx @@ -20,7 +20,7 @@ import { Badge } from '@coinbase/onchainkit/identity'; ``` ```html [return html] - + - zizzamia.eth -
- +
+ nickprince.eth +
+ +
zizzamia.eth -
-
- +
+
+ + +## CSS + +```css +.ock-badge { + @apply rounded-[50%] border; +} +.ock-withavatarbadge-container { + @apply relative h-8 w-8; +} +.ock-withavatarbadge-badge { + @apply absolute -bottom-0.5 -right-0.5 flex h-[15px] w-[15px] items-center justify-center rounded-full bg-transparent; +} +.ock-withavatarbadge-inner { + @apply flex h-[11px] w-[11px] items-center justify-center; +} +.ock-withnamebadge-container { + @apply flex items-center; +} +.ock-withnamebadge-inner { + @apply ml-1; +} +``` diff --git a/site/docs/pages/identity/introduction.mdx b/site/docs/pages/identity/introduction.mdx index 0f6b940d07..7c76c6bd95 100644 --- a/site/docs/pages/identity/introduction.mdx +++ b/site/docs/pages/identity/introduction.mdx @@ -20,9 +20,7 @@ The available components are: - [``](/identity/badge): Display an Attestation badge. - [``](/identity/name): Display an ENS name. -:::code-group - -```tsx [code] +```tsx ``` -```html [return html] -
-
- nickprince.eth -
-
- - - - - - -
-
-
-
- nickprince.eth - 0x838...B4D9 -
-
-``` - -::: - diff --git a/site/docs/pages/onchainkit-provider.mdx b/site/docs/pages/onchainkit-provider.mdx deleted file mode 100644 index 2475402c9b..0000000000 --- a/site/docs/pages/onchainkit-provider.mdx +++ /dev/null @@ -1,24 +0,0 @@ -# `` - -Provides the OnchainKit React Context to the app. - -## Usage - -```tsx [code] -import { base } from 'viem/chains'; // [!code focus] -import { OnchainKitProvider } from '@coinbase/onchainkit'; // [!code focus] - -const App = () => { - return ( - - {' '} - // [!code focus] - - // [!code focus] - ); -}; -``` - -## Props - -[`OnchainKitProviderReact`](/types#onchainkitproviderreact) diff --git a/site/docs/pages/token/introduction.mdx b/site/docs/pages/token/introduction.mdx index cb345680d7..5724a00dc8 100644 --- a/site/docs/pages/token/introduction.mdx +++ b/site/docs/pages/token/introduction.mdx @@ -20,9 +20,7 @@ The available components are: - [``](/token/token-search): Search component to search by name, symbol and address for a given list of tokens. - [``](/token/token-selector): Stylized button component to display token info or placeholder text. -:::code-group - -```tsx [code] +```tsx const [filteredTokens, setFilteredTokens] = useState([]); const handleChange = useCallback((value) => { @@ -59,195 +57,6 @@ const handleChange = useCallback((value) => {
``` -```html [return html] -
-
-
- - - -
- -
-
- - - - -
-
-
Tokens
-
- - - - -
-
-
-``` - -::: - @@ -280,6 +89,11 @@ pnpm add @coinbase/onchainkit pnpm add viem@2.x react@18 react-dom@18 ``` +```bash [bun] +bun add @coinbase/onchainkit +bun add viem@2.x react@18 react-dom@18 +``` + ::: ## Required providers diff --git a/site/docs/pages/token/token-chip.mdx b/site/docs/pages/token/token-chip.mdx index 0307084852..208c229e76 100644 --- a/site/docs/pages/token/token-chip.mdx +++ b/site/docs/pages/token/token-chip.mdx @@ -66,16 +66,13 @@ const token = { &:hover { background: #cacbce; } - &:active { background: #bfc1c3; } } - .ock-tokenchip-label { @apply text-base font-medium leading-4 text-black; } - .ock-tokenchip-image { @apply mr-2 h-6 w-6; } diff --git a/site/docs/pages/token/token-image.mdx b/site/docs/pages/token/token-image.mdx index c03817e431..d0187fcb39 100644 --- a/site/docs/pages/token/token-image.mdx +++ b/site/docs/pages/token/token-image.mdx @@ -5,6 +5,8 @@ import App from '../App'; The `TokenImage` component is an image that crops token image to a circle with an adjustable size. +With `token` props has no image, render partial token symbol and deterministic dark color. + ## Usage `TokenImage` with an url @@ -14,12 +16,12 @@ The `TokenImage` component is an image that crops token image to a circle with a ```tsx [code] ``` @@ -27,13 +29,13 @@ The `TokenImage` component is an image that crops token image to a circle with a ``` @@ -42,13 +44,29 @@ The `TokenImage` component is an image that crops token image to a circle with a
@@ -60,29 +78,45 @@ The `TokenImage` component is an image that crops token image to a circle with a ```tsx [code] ``` ```html [return html]
-
+
-
+
``` @@ -91,13 +125,29 @@ The `TokenImage` component is an image that crops token image to a circle with a
@@ -106,3 +156,11 @@ The `TokenImage` component is an image that crops token image to a circle with a ## Props [`TokenImageReact`](/token/types#tokenselectorreact) + +## CSS + +```css +.ock-tokenimage { + @apply overflow-hidden rounded-[50%]; +} +``` diff --git a/site/docs/pages/token/token-row.mdx b/site/docs/pages/token/token-row.mdx index 1f1901c00a..88d6459395 100644 --- a/site/docs/pages/token/token-row.mdx +++ b/site/docs/pages/token/token-row.mdx @@ -14,62 +14,42 @@ Token with an image url ```tsx [code] import { TokenRow } from '@coinbase/onchainkit/token'; -const token = { - address: '0x1234', - chainId: 1, - decimals: 18, - image: - 'https://dynamic-assets.coinbase.com/dbb4b4983bde81309ddab83eb598358eb44375b930b94687ebe38bc22e52c3b2125258ffb8477a5ef22e33d6bd72e32a506c391caa13af64c00e46613c3e5806/asset_icons/4113b082d21cc5fab17fc8f2d19fb996165bcce635e6900f7fc2d57c4ef33ae9.png', - name: 'Ether', - symbol: 'ETH', -}; +const token = { ... }; ; // [!code focus] ``` ```html [return html] - ``` ::: -
- -
+
Token without an image url @@ -79,61 +59,43 @@ Token without an image url ```tsx [code] import { TokenRow } from '@coinbase/onchainkit/token'; -const token = { - address: '0x1234', - chainId: 1, - decimals: 18, - image: null, - name: 'Ether', - symbol: 'ETH', -}; +const token = { ... }; ; // [!code focus] ``` ```html [return html] - ``` ::: -
- -
+
Token with an amount @@ -143,106 +105,199 @@ Token with an amount ```tsx [code] import { TokenRow } from '@coinbase/onchainkit/token'; -const token = { - address: '0x1234', - chainId: 1, - decimals: 18, - image: - 'https://dynamic-assets.coinbase.com/dbb4b4983bde81309ddab83eb598358eb44375b930b94687ebe38bc22e52c3b2125258ffb8477a5ef22e33d6bd72e32a506c391caa13af64c00e46613c3e5806/asset_icons/4113b082d21cc5fab17fc8f2d19fb996165bcce635e6900f7fc2d57c4ef33ae9.png', - name: 'Ether', - symbol: 'ETH', -}; +const token = { ... }; ; // [!code focus] ; // [!code focus] ``` ```html [return html] - - ``` ::: -
- -
-
- + + + +Variations with `hideImage` and `hideSymbol` + +:::code-group + +```tsx [code] +import { TokenRow } from '@coinbase/onchainkit/token'; + +const token = { ... }; + +; // [!code focus] +; // [!code focus] +; // [!code focus] +``` + +```html [return html] +
+ + Ethereum + + + + + + + + +``` + +::: + + + + +{' '} + + + + ## Props [`TokenRowReact`](/token/types#tokenrowreact) + +## CSS + +```css +.ock-tokenrow-button { + @apply flex h-16 w-full cursor-pointer items-center justify-between px-2 py-1; + background: #ffffff; + + &:hover { + background: #cacbce; + } + &:active { + background: #bfc1c3; + } +} +.ock-tokenrow-left { + @apply flex items-center gap-3; +} +.ock-tokenrow-body { + @apply flex flex-col items-start; +} +.ock-tokenrow-name { + @apply text-base font-medium leading-normal text-[#0A0B0D]; +} +.ock-tokenrow-symbol { + @apply text-base font-normal leading-normal text-[#5B616E]; +} +.ock-tokenrow-data { + @apply text-base font-normal leading-normal text-[#5B616E]; +} +``` diff --git a/site/docs/pages/token/token-search.mdx b/site/docs/pages/token/token-search.mdx index 6af766bdbe..f8a90a9ba6 100644 --- a/site/docs/pages/token/token-search.mdx +++ b/site/docs/pages/token/token-search.mdx @@ -40,8 +40,8 @@ const handleChange = useCallback((value) => { ``` ```html [return html] -
-
+
+
{
``` @@ -75,31 +75,24 @@ const handleChange = useCallback((value) => { .ock-textinput-container { @apply relative flex items-center; } - .ock-textinput-iconsearch { @apply absolute left-4 top-2/4 -translate-y-2/4; } - .ock-textinput-input { @apply w-full rounded-full border-2 border-solid border-[#eef0f3] py-2 pl-12 pr-5 text-[#0A0B0D]; background: #eef0f3; + outline: none; &::placeholder { @apply text-[#5B616E]; } &:hover { - @apply border-[#cacbce]; background: #cacbce; } - &:focus { - @apply border-[#0052FF]; - outline: none; - } &:hover:focus { background: #eef0f3; } } - .ock-textinput-clearbutton { @apply absolute right-4 top-2/4 -translate-y-2/4; } diff --git a/site/docs/pages/token/token-selector.mdx b/site/docs/pages/token/token-selector.mdx index 6cd2561772..3e2ae229f7 100644 --- a/site/docs/pages/token/token-selector.mdx +++ b/site/docs/pages/token/token-selector.mdx @@ -1,107 +1,189 @@ -import { TokenSelector } from '@coinbase/onchainkit/token'; +import { TokenSelector, TokenSelectorDropdown } from '@coinbase/onchainkit/token'; +import TokenSelectorContainer from '../../components/TokenSelectorContainer.tsx'; import App from '../App'; # `` `TokenSelector` component is stylized button component that displays the token info or placeholder text. +`TokenSelectorDropdown` component is a dropdown component for `TokenSelector`. + ## Usage -`TokenSelector` with token prop +`TokenSelector` with `TokenSelectorDropdown` :::code-group ```tsx [code] - {}} -/> + // [!code focus] + // [!code focus] + // [!code focus] ``` ```html [return html] - + +
+
+ + + +
+
+
``` ::: - {}} - /> + + {(token, setToken) => ( + + + + )} + -`TokenSelector` with no token prop - -:::code-group +## Props -```tsx [code] - {}} /> +[`TokenSelectorReact`](/token/types#tokenselectorreact) +[`TokenSelectorDropdownReact`](/token/types#tokenselectordropdownreact) + +## CSS + +`TokenSelector` + +```css +.ock-tokenselector-container { + @apply relative; +} +.ock-tokenselector-button { + @apply flex w-fit items-center rounded-2xl px-3 py-1; + background: #eef0f3; + gap: 8px; + outline: none; + &:hover { + background: #cacbce; + } + &:active { + background: #bfc1c3; + } +} +.ock-tokenselector-label { + @apply text-base font-medium leading-normal text-[#0a0b0d]; +} ``` -```html [return html] - +`TokenSelectorDropdown` + +```css +.ock-tokenselectordropdown-container { + @apply absolute z-10 mt-1 flex max-h-80 w-[250px] flex-col overflow-y-hidden rounded-lg; + scrollbar-width: thin; + scrollbar-color: #eef0f3 #ffffff; +} +.ock-tokenselectordropdown-scroll { + @apply overflow-y-auto; +} ``` - -::: - - - {}} /> - - -## Props - -[`TokenSelectorReact`](/token/types#tokenselectorreact) diff --git a/site/docs/pages/token/types.mdx b/site/docs/pages/token/types.mdx index 09a0979fad..ee0aa8c9be 100644 --- a/site/docs/pages/token/types.mdx +++ b/site/docs/pages/token/types.mdx @@ -93,8 +93,19 @@ export type TokenSearchReact = { ```ts export type TokenSelectorReact = { - token: Token; // Rendered token - onClick: () => void; // Component on click handler + children: ReactElement<{ onToggle: () => void }>; // Should either be a dropdown or modal that handle token selection + setToken: (token: Token) => void; // Token setter + token?: Token; // Selected token +}; +``` + +## `TokenSelectorDropdownReact` + +```ts +export type TokenSelectorDropdownReact = { + onToggle: () => void; // Injected by TokenSelector. To be removed. + options: Token[]; // List of tokens + setToken: (token: Token) => void; // Token setter }; ``` diff --git a/site/docs/pages/wallet/connect-account.mdx b/site/docs/pages/wallet/connect-account.mdx index ea5962dc53..faac19710d 100644 --- a/site/docs/pages/wallet/connect-account.mdx +++ b/site/docs/pages/wallet/connect-account.mdx @@ -69,3 +69,17 @@ function AccountConnect() { ## Props [`ConnectAccountReact`](/wallet/types#connectaccountreact) + +## CSS + +```css +.ock-connectaccount { + @apply flex flex-grow; +} +.ock-connectaccount-button { + @apply inline-flex h-10 grow items-center justify-center gap-2 rounded-3xl bg-white px-4 py-2; +} +.ock-connectaccount-inner { + @apply text-sm font-medium leading-5 text-black; +} +``` diff --git a/site/docs/pages/wallet/introduction.mdx b/site/docs/pages/wallet/introduction.mdx index 50f72fa063..8d15239c68 100644 --- a/site/docs/pages/wallet/introduction.mdx +++ b/site/docs/pages/wallet/introduction.mdx @@ -16,9 +16,7 @@ The available components are: - [``](/wallet/connect-account): Renders the connect button -:::code-group - -```tsx [code] +```tsx ...
@@ -42,26 +40,6 @@ The available components are: ``` -```html [return html] - -``` - -::: - @@ -87,6 +65,11 @@ pnpm add @coinbase/onchainkit pnpm add react@18 react-dom@18 wagmi@2 @coinbase/wallet-sdk@4 permissionless ``` +```bash [bun] +bun add @coinbase/onchainkit +bun add react@18 react-dom@18 wagmi@2 @coinbase/wallet-sdk@4 permissionless +``` + ::: ## Required providers diff --git a/site/docs/pages/xmtp/introduction.mdx b/site/docs/pages/xmtp/introduction.mdx index 4e91c9434d..afffd32ed6 100644 --- a/site/docs/pages/xmtp/introduction.mdx +++ b/site/docs/pages/xmtp/introduction.mdx @@ -53,6 +53,10 @@ yarn add @coinbase/onchainkit @xmtp/frames-validator pnpm add @coinbase/onchainkit @xmtp/frames-validator ``` +```bash [bun] +bun add @coinbase/onchainkit @xmtp/frames-validator +``` + ::: To assist you in handling interactions from XMTP, and extracting the `verifiedWalletAddress` from a POST payload, here is the XMTP Kit which includes: diff --git a/site/docs/public/assets/copy-api-key.png b/site/docs/public/assets/copy-api-key.png new file mode 100644 index 0000000000..e257c9f831 Binary files /dev/null and b/site/docs/public/assets/copy-api-key.png differ diff --git a/site/docs/public/logo/v0-19.png b/site/docs/public/logo/v0-19.png new file mode 100644 index 0000000000..f128c1260b Binary files /dev/null and b/site/docs/public/logo/v0-19.png differ diff --git a/site/package.json b/site/package.json index 51d6d00a01..ab7b06aa5d 100644 --- a/site/package.json +++ b/site/package.json @@ -8,7 +8,7 @@ "preview": "vocs preview" }, "dependencies": { - "@coinbase/onchainkit": "0.18.6", + "@coinbase/onchainkit": "0.19.6", "@coinbase/wallet-sdk": "^4.0.0", "@tanstack/react-query": "^5.36.0", "@types/react": "latest", diff --git a/site/sidebar.ts b/site/sidebar.ts index cfab10aee4..0fed9daae5 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -3,14 +3,26 @@ import type { Sidebar } from 'vocs'; export const sidebar = [ { text: 'Introduction', + items: [{ text: 'Getting Started', link: '/getting-started' }], + }, + { + text: 'Guides', + items: [ + { + text: 'Contribution', + link: '/guides/contribution', + }, + ], + }, + { + text: 'Config', items: [ - { text: 'Getting Started', link: '/getting-started' }, { text: 'Components', items: [ { text: 'OnchainKitProvider', - link: '/onchainkit-provider', + link: '/config/onchainkit-provider', }, ], }, @@ -20,17 +32,13 @@ export const sidebar = [ items: [ { text: 'isBase', - link: '/is-base', + link: '/config/is-base', }, ], }, { text: 'Types', - link: '/types', - }, - { - text: 'Contribution Guide', - link: '/contribution-guide', + link: '/config/types', }, ], }, diff --git a/site/yarn.lock b/site/yarn.lock index e445af1ef6..7987a0fcf1 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -334,9 +334,9 @@ __metadata: languageName: node linkType: hard -"@coinbase/onchainkit@npm:0.18.6": - version: 0.18.6 - resolution: "@coinbase/onchainkit@npm:0.18.6" +"@coinbase/onchainkit@npm:0.19.6": + version: 0.19.6 + resolution: "@coinbase/onchainkit@npm:0.19.6" peerDependencies: "@coinbase/wallet-sdk": ^4 "@tanstack/react-query": ^5 @@ -348,7 +348,7 @@ __metadata: react-dom: ^18 viem: ^2 wagmi: ^2 - checksum: 5feeb2c09bbec0549e26bc777238593e94ce1fa76f2b29c4bb7bf75ca46015a54a0fd284d26e4d5f3a66794118acf7a877830938a15add42825c85cb134fe694 + checksum: 3732db3903b63db7c65670f570cffb4013329c7bf83027c1416f38996cfd76c41b23f054daec2e3fad05c5322c144a72f5fa9bbfde97a04205fbdad67805c36e languageName: node linkType: hard @@ -7657,7 +7657,7 @@ __metadata: version: 0.0.0-use.local resolution: "onchainkit@workspace:." dependencies: - "@coinbase/onchainkit": "npm:0.18.6" + "@coinbase/onchainkit": "npm:0.19.6" "@coinbase/wallet-sdk": "npm:^4.0.0" "@tanstack/react-query": "npm:^5.36.0" "@types/react": "npm:latest" diff --git a/src/OnchainKitConfig.test.ts b/src/OnchainKitConfig.test.ts index 59c951e89c..3b8a27b33e 100644 --- a/src/OnchainKitConfig.test.ts +++ b/src/OnchainKitConfig.test.ts @@ -1,4 +1,3 @@ -import fs from 'fs'; import { baseSepolia } from 'viem/chains'; import { getOnchainKitConfig, setOnchainKitConfig } from './OnchainKitConfig'; import { getRPCUrl } from './getRPCUrl'; @@ -24,7 +23,7 @@ describe('OnchainKitConfig', () => { const chain = baseSepolia; const schemaId = '0x123'; const apiKey = 'test-api-key'; - const rpcUrl = `https://api.developer.coinbase.com/rpc/v1/base-sepolia/test-api-key`; + const rpcUrl = 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/test-api-key'; setOnchainKitConfig({ chain, schemaId, apiKey }); expect(getOnchainKitConfig('chain')).toEqual(chain); expect(getOnchainKitConfig('schemaId')).toEqual(schemaId); @@ -36,7 +35,7 @@ describe('OnchainKitConfig', () => { const chain = baseSepolia; const schemaId = '0x123'; const apiKey = 'updated-api-key'; - const rpcUrl = `https://api.developer.coinbase.com/rpc/v1/base-sepolia/updated-api-key`; + const rpcUrl = 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/updated-api-key'; setOnchainKitConfig({ chain, schemaId, apiKey, rpcUrl }); expect(getOnchainKitConfig('chain')).toEqual(chain); expect(getOnchainKitConfig('schemaId')).toEqual(schemaId); diff --git a/src/OnchainKitConfig.ts b/src/OnchainKitConfig.ts index cad3ef2f55..d774eaffc3 100644 --- a/src/OnchainKitConfig.ts +++ b/src/OnchainKitConfig.ts @@ -1,5 +1,5 @@ import { baseSepolia } from 'viem/chains'; -import { OnchainKitConfig, SetOnchainKitConfig } from './types'; +import type { OnchainKitConfig, SetOnchainKitConfig } from './types'; // The ONCHAIN_KIT_CONFIG is not exported at index.ts, // but only acccessed through the get and set functions. diff --git a/src/OnchainKitProvider.test.tsx b/src/OnchainKitProvider.test.tsx index f5a6db2f14..1b70d599c2 100644 --- a/src/OnchainKitProvider.test.tsx +++ b/src/OnchainKitProvider.test.tsx @@ -6,7 +6,8 @@ import { base } from 'viem/chains'; import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { EASSchemaUid } from './identity/types'; +import type { EASSchemaUid } from './identity/types'; +import { setOnchainKitConfig, ONCHAIN_KIT_CONFIG } from './OnchainKitConfig'; import { OnchainKitProvider } from './OnchainKitProvider'; import { useOnchainKit } from './useOnchainKit'; @@ -20,6 +21,17 @@ const TestComponent = () => { ); }; +jest.mock('./OnchainKitConfig', () => ({ + setOnchainKitConfig: jest.fn(), + ONCHAIN_KIT_CONFIG: { + address: null, + apiKey: null, + chain: base, + rpcUrl: null, + schemaId: null, + }, +})); + describe('OnchainKitProvider', () => { const schemaId: EASSchemaUid = `0x${'1'.repeat(64)}`; const apiKey = 'test-api-key'; @@ -66,4 +78,19 @@ describe('OnchainKitProvider', () => { ), ).not.toThrow(); }); + + it('should call setOnchainKitConfig with the correct values', async () => { + render( + + + , + ); + expect(setOnchainKitConfig).toHaveBeenCalledWith({ + address: null, + apiKey, + chain: base, + rpcUrl: null, + schemaId, + }); + }); }); diff --git a/src/OnchainKitProvider.tsx b/src/OnchainKitProvider.tsx index f84460f912..4924f97991 100644 --- a/src/OnchainKitProvider.tsx +++ b/src/OnchainKitProvider.tsx @@ -1,7 +1,7 @@ import { createContext, useMemo } from 'react'; import { checkHashLength } from './utils/checkHashLength'; -import { ONCHAIN_KIT_CONFIG } from './OnchainKitConfig'; -import { OnchainKitContextType, OnchainKitProviderReact } from './types'; +import { ONCHAIN_KIT_CONFIG, setOnchainKitConfig } from './OnchainKitConfig'; +import type { OnchainKitContextType, OnchainKitProviderReact } from './types'; export const OnchainKitContext = createContext(ONCHAIN_KIT_CONFIG); @@ -20,13 +20,15 @@ export function OnchainKitProvider({ throw Error('EAS schemaId must be 64 characters prefixed with "0x"'); } const value = useMemo(() => { - return { + const onchainKitConfig = { address: address ?? null, apiKey: apiKey ?? null, chain: chain, rpcUrl: rpcUrl ?? null, schemaId: schemaId ?? null, }; + setOnchainKitConfig(onchainKitConfig); + return onchainKitConfig; }, [address, chain, schemaId, apiKey, rpcUrl]); return {children}; } diff --git a/src/definitions/base.ts b/src/definitions/base.ts index 72e0cec056..463586c3bc 100644 --- a/src/definitions/base.ts +++ b/src/definitions/base.ts @@ -1,5 +1,5 @@ import { base } from 'viem/chains'; -import { EASChainDefinition } from '../identity/types'; +import type { EASChainDefinition } from '../identity/types'; export const easChainBase: EASChainDefinition = { id: base.id, diff --git a/src/definitions/baseSepolia.ts b/src/definitions/baseSepolia.ts index 386d23a9cb..85346bdf62 100644 --- a/src/definitions/baseSepolia.ts +++ b/src/definitions/baseSepolia.ts @@ -1,5 +1,5 @@ import { baseSepolia } from 'viem/chains'; -import { EASChainDefinition } from '../identity/types'; +import type { EASChainDefinition } from '../identity/types'; export const easChainBaseSepolia: EASChainDefinition = { id: baseSepolia.id, diff --git a/src/definitions/optimism.ts b/src/definitions/optimism.ts index 674cf842f2..df31b3845c 100644 --- a/src/definitions/optimism.ts +++ b/src/definitions/optimism.ts @@ -1,5 +1,5 @@ import { optimism } from 'viem/chains'; -import { EASChainDefinition } from '../identity/types'; +import type { EASChainDefinition } from '../identity/types'; // More details in https://docs.optimism.io/chain/identity/schemas export const easChainOptimism: EASChainDefinition = { diff --git a/src/definitions/swap.ts b/src/definitions/swap.ts index c128dc056a..0269eb5470 100644 --- a/src/definitions/swap.ts +++ b/src/definitions/swap.ts @@ -1 +1,2 @@ -export const ListSwapAssets = 'cdp_listSwapAssets'; +export const CDP_LISTSWAPASSETS = 'cdp_listSwapAssets'; +export const CDP_GETSWAPQUOTE = 'cdp_getSwapQuote'; diff --git a/src/farcaster/getFarcasterUserAddress.ts b/src/farcaster/getFarcasterUserAddress.ts index 8a7820b9c4..9d20b377b3 100644 --- a/src/farcaster/getFarcasterUserAddress.ts +++ b/src/farcaster/getFarcasterUserAddress.ts @@ -1,6 +1,6 @@ import { getCustodyAddressForFidNeynar } from '../utils/neynar/user/getCustodyAddressForFidNeynar'; import { getVerifiedAddressesForFidNeynar } from '../utils/neynar/user/getVerifiedAddressesForFidNeynar'; -import { GetFarcasterUserAddressResponse } from './types'; +import type { GetFarcasterUserAddressResponse } from './types'; type GetFarcasterUserAddressOptions = | { diff --git a/src/frame/components/FrameMetadata.tsx b/src/frame/components/FrameMetadata.tsx index 099b0492d5..5c80c39bb7 100644 --- a/src/frame/components/FrameMetadata.tsx +++ b/src/frame/components/FrameMetadata.tsx @@ -2,49 +2,7 @@ import { Fragment } from 'react'; import type { FrameMetadataReact } from '../types'; /** - * FrameMetadata component - * - * @description * This component is used to add React Frame Metadata to the page. - * - * @example - * ```tsx - * - * ``` - * - * @param {FrameMetadataReact} props - The metadata for the frame. - * @param {{ [protocolIdentifier: string]: string; }} accepts - The types of protocol the frame accepts. - * @param {Array<{ label: string, action?: string }>} props.buttons - The buttons. - * @param {string | { src: string, aspectRatio?: string }} props.image - The image URL. - * @param {string} props.input - The input text. - * @param {boolean} props.isOpenFrame: Whether the frame uses the Open Frames standard. - * @param {string} props.ogDescription - The Open Graph description. - * @param {string} props.ogTitle - The Open Graph title. - * @param {string} props.postUrl - The post URL. - * @param {number} props.refreshPeriod - The refresh period. - * @param {object} props.state - The serialized state (e.g. JSON) for the frame. - * @param {React.ComponentType | undefined} props.wrapper - The wrapper component meta tags are rendered in. - * @returns {React.ReactElement} The FrameMetadata component. */ export function FrameMetadata({ accepts = {}, diff --git a/src/getRPCUrl.test.ts b/src/getRPCUrl.test.ts index e74392d1d0..73e1d03fed 100644 --- a/src/getRPCUrl.test.ts +++ b/src/getRPCUrl.test.ts @@ -14,7 +14,7 @@ describe('OnchainKitConfig RPC URL', () => { it('should return the correct config value', () => { const chain = baseSepolia; const apiKey = 'test-api-key'; - const rpcUrl = `https://api.developer.coinbase.com/rpc/v1/base-sepolia/test-api-key`; + const rpcUrl = 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/test-api-key'; setOnchainKitConfig({ chain, apiKey }); expect(getOnchainKitConfig('chain')).toEqual(chain); expect(getOnchainKitConfig('apiKey')).toEqual(apiKey); @@ -25,7 +25,7 @@ describe('OnchainKitConfig RPC URL', () => { const chain = baseSepolia; const apiKey = 'test-api-key'; const newApiKey = 'updated-api-key'; - const expectedRpcUrl = `https://api.developer.coinbase.com/rpc/v1/base-sepolia/updated-api-key`; + const expectedRpcUrl = 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/updated-api-key'; setOnchainKitConfig({ chain, apiKey }); expect(getOnchainKitConfig('chain')).toEqual(chain); expect(getOnchainKitConfig('apiKey')).toEqual(apiKey); diff --git a/src/identity/components/Avatar.css b/src/identity/components/Avatar.css new file mode 100644 index 0000000000..86cecc7352 --- /dev/null +++ b/src/identity/components/Avatar.css @@ -0,0 +1,3 @@ +.ock-avatar { + @apply rounded-[50%]; +} diff --git a/src/identity/components/Avatar.test.tsx b/src/identity/components/Avatar.test.tsx index a2a2967a73..20734d7113 100644 --- a/src/identity/components/Avatar.test.tsx +++ b/src/identity/components/Avatar.test.tsx @@ -108,7 +108,7 @@ describe('Avatar Component', () => { render(); await waitFor(() => { - const inner = screen.getByTestId('ockAvatarBadgeInner'); + const inner = screen.getByTestId('ockAvatarBadgeContainer'); expect(inner).toBeInTheDocument(); const badge = screen.getByTestId('ockBadge'); expect(badge).toBeInTheDocument(); diff --git a/src/identity/components/Avatar.tsx b/src/identity/components/Avatar.tsx index 6994684cdc..c60fb25a3e 100644 --- a/src/identity/components/Avatar.tsx +++ b/src/identity/components/Avatar.tsx @@ -80,14 +80,13 @@ export function Avatar({ return ( {name} diff --git a/src/identity/components/Badge.css b/src/identity/components/Badge.css new file mode 100644 index 0000000000..0d04c7eeab --- /dev/null +++ b/src/identity/components/Badge.css @@ -0,0 +1,3 @@ +.ock-badge { + @apply rounded-[50%] border border-transparent; +} diff --git a/src/identity/components/Badge.test.tsx b/src/identity/components/Badge.test.tsx index 28cb019085..c61b84ff97 100644 --- a/src/identity/components/Badge.test.tsx +++ b/src/identity/components/Badge.test.tsx @@ -21,7 +21,7 @@ describe('WithAvatarBadge Component', () => { await waitFor(() => { const badge = screen.queryByTestId('ockBadge'); - expect(badge).toHaveStyle('border-radius: 50%; height: 12px; width: 12px;'); + expect(badge).toHaveStyle('height: 12px; width: 12px;'); const bg = screen.queryByTestId('ockBadgeBackground'); expect(bg).toHaveAttribute('fill', '#0052FF'); const ticker = screen.queryByTestId('ockBadgeTicker'); @@ -34,9 +34,7 @@ describe('WithAvatarBadge Component', () => { await waitFor(() => { const badge = screen.queryByTestId('ockBadge'); - expect(badge).toHaveStyle( - 'border: 1px solid red; border-radius: 50%; height: 14px; width: 14px;', - ); + expect(badge).toHaveStyle('border-color: red; height: 14px; width: 14px;'); const bg = screen.queryByTestId('ockBadgeBackground'); expect(bg).toHaveAttribute('fill', '#0052FF'); const ticker = screen.queryByTestId('ockBadgeTicker'); @@ -49,7 +47,7 @@ describe('WithAvatarBadge Component', () => { await waitFor(() => { const badge = screen.queryByTestId('ockBadge'); - expect(badge).toHaveStyle('border-radius: 50%; height: 12px; width: 12px;'); + expect(badge).toHaveStyle('height: 12px; width: 12px;'); const bg = screen.queryByTestId('ockBadgeBackground'); expect(bg).toHaveAttribute('fill', 'green'); const ticker = screen.queryByTestId('ockBadgeTicker'); @@ -62,7 +60,7 @@ describe('WithAvatarBadge Component', () => { await waitFor(() => { const badge = screen.queryByTestId('ockBadge'); - expect(badge).toHaveStyle('border-radius: 50%; height: 12px; width: 12px;'); + expect(badge).toHaveStyle('height: 12px; width: 12px;'); const bg = screen.queryByTestId('ockBadgeBackground'); expect(bg).toHaveAttribute('fill', '#0052FF'); const ticker = screen.queryByTestId('ockBadgeTicker'); diff --git a/src/identity/components/Badge.tsx b/src/identity/components/Badge.tsx index c0af5a99b7..c725d22ff1 100644 --- a/src/identity/components/Badge.tsx +++ b/src/identity/components/Badge.tsx @@ -2,8 +2,6 @@ import { BadgeReact } from '../types'; /** * Badge component. - * - * @returns {JSX.Element} The JSX element representing the badge, which is a blue circle with a white checkmark. */ export function Badge({ backgroundColor = '#0052FF', @@ -15,11 +13,10 @@ export function Badge({ const badgeSize = borderColor ? '14px' : '12px'; return ( { ); await waitFor(() => { - const inner = screen.queryByTestId('ockAvatarBadgeInner'); + const inner = screen.queryByTestId('ockAvatarBadgeContainer'); expect(inner).toBeNull(); }); }); @@ -54,7 +54,7 @@ describe('WithAvatarBadge Component', () => { ); await waitFor(() => { - const inner = screen.getByTestId('ockAvatarBadgeInner'); + const inner = screen.getByTestId('ockAvatarBadgeContainer'); expect(inner).toBeInTheDocument(); const badge = screen.queryByTestId('ockBadge'); expect(badge).toBeNull(); @@ -76,7 +76,7 @@ describe('WithAvatarBadge Component', () => { ); await waitFor(() => { - const inner = screen.getByTestId('ockAvatarBadgeInner'); + const inner = screen.getByTestId('ockAvatarBadgeContainer'); expect(inner).toBeInTheDocument(); const badge = screen.getByTestId('ockBadge'); expect(badge).toBeInTheDocument(); diff --git a/src/identity/components/WithAvatarBadge.tsx b/src/identity/components/WithAvatarBadge.tsx index 41b136a997..b3d5709500 100644 --- a/src/identity/components/WithAvatarBadge.tsx +++ b/src/identity/components/WithAvatarBadge.tsx @@ -1,24 +1,12 @@ -import type { Address } from 'viem'; - import { useOnchainKit } from '../../useOnchainKit'; import { useAttestations } from '../hooks/useAttestations'; import { Badge } from './Badge'; - -type WithAvatarBadgeInnerProps = { - children: React.ReactNode; - address: Address; -}; - -type WithAvatarBadgeProps = { - children: React.ReactNode; - showAttestation: boolean; - address: Address; -}; +import type { WithAvatarBadgeInnerReact, WithAvatarBadgeReact } from '../types'; const ERROR_MESSAGE = 'EAS schemaId must provided in OnchainKitProvider context when using WithNameBadge showAttestation is true.'; -function WithAvatarBadgeInner({ children, address }: WithAvatarBadgeInnerProps) { +function WithAvatarBadgeInner({ children, address }: WithAvatarBadgeInnerReact) { const onchainKitContext = useOnchainKit(); // SchemaId is required to fetch attestations if (!onchainKitContext?.schemaId) { @@ -31,35 +19,11 @@ function WithAvatarBadgeInner({ children, address }: WithAvatarBadgeInnerProps) schemaId: onchainKitContext?.schemaId, }); return ( -
+
{children} {attestations && attestations[0] && ( -
-
+
+
@@ -68,7 +32,7 @@ function WithAvatarBadgeInner({ children, address }: WithAvatarBadgeInnerProps) ); } -export function WithAvatarBadge({ children, showAttestation, address }: WithAvatarBadgeProps) { +export function WithAvatarBadge({ children, showAttestation, address }: WithAvatarBadgeReact) { if (!showAttestation) { return children; } diff --git a/src/identity/components/WithNameBadge.css b/src/identity/components/WithNameBadge.css new file mode 100644 index 0000000000..46638e458d --- /dev/null +++ b/src/identity/components/WithNameBadge.css @@ -0,0 +1,6 @@ +.ock-withnamebadge-container { + @apply flex items-center; +} +.ock-withnamebadge-inner { + @apply ml-1; +} diff --git a/src/identity/components/WithNameBadge.test.tsx b/src/identity/components/WithNameBadge.test.tsx index 236d17b384..92ceb9d345 100644 --- a/src/identity/components/WithNameBadge.test.tsx +++ b/src/identity/components/WithNameBadge.test.tsx @@ -35,7 +35,7 @@ describe('WithNameBadge Component', () => { ); await waitFor(() => { - const inner = screen.queryByTestId('ockNameBadgeInner'); + const inner = screen.queryByTestId('ockNameBadgeContainer'); expect(inner).toBeNull(); }); }); @@ -54,7 +54,7 @@ describe('WithNameBadge Component', () => { ); await waitFor(() => { - const inner = screen.getByTestId('ockNameBadgeInner'); + const inner = screen.getByTestId('ockNameBadgeContainer'); expect(inner).toBeInTheDocument(); const badge = screen.queryByTestId('ockBadge'); expect(badge).toBeNull(); @@ -76,7 +76,7 @@ describe('WithNameBadge Component', () => { ); await waitFor(() => { - const inner = screen.getByTestId('ockNameBadgeInner'); + const inner = screen.getByTestId('ockNameBadgeContainer'); expect(inner).toBeInTheDocument(); const badge = screen.getByTestId('ockBadge'); expect(badge).toBeInTheDocument(); diff --git a/src/identity/components/WithNameBadge.tsx b/src/identity/components/WithNameBadge.tsx index 2acf751968..68e936c1db 100644 --- a/src/identity/components/WithNameBadge.tsx +++ b/src/identity/components/WithNameBadge.tsx @@ -1,24 +1,12 @@ -import type { Address } from 'viem'; - import { useOnchainKit } from '../../useOnchainKit'; import { useAttestations } from '../hooks/useAttestations'; import { Badge } from './Badge'; - -type WithNameBadgeInnerProps = { - children: React.ReactNode; - address: Address; -}; - -type WithNameBadgeProps = { - children: React.ReactNode; - showAttestation?: boolean; - address: Address; -}; +import type { WithNameBadgeInnerReact, WithNameBadgeReact } from '../types'; const ERROR_MESSAGE = 'EAS schemaId must provided in OnchainKitProvider context when using WithNameBadge showAttestation is true.'; -function WithNameBadgeInner({ children, address }: WithNameBadgeInnerProps) { +function WithNameBadgeInner({ children, address }: WithNameBadgeInnerReact) { const onchainKitContext = useOnchainKit(); // SchemaId is required to fetch attestations if (!onchainKitContext?.schemaId) { @@ -31,10 +19,10 @@ function WithNameBadgeInner({ children, address }: WithNameBadgeInnerProps) { schemaId: onchainKitContext?.schemaId, }); return ( -
+
{children} {attestations && attestations[0] && ( -
+
)} @@ -42,7 +30,7 @@ function WithNameBadgeInner({ children, address }: WithNameBadgeInnerProps) { ); } -export function WithNameBadge({ children, showAttestation, address }: WithNameBadgeProps) { +export function WithNameBadge({ children, showAttestation, address }: WithNameBadgeReact) { if (!showAttestation) { return children; } diff --git a/src/identity/core/getAvatar.test.tsx b/src/identity/core/getAvatar.test.tsx index 527aca4232..cc2a47698a 100644 --- a/src/identity/core/getAvatar.test.tsx +++ b/src/identity/core/getAvatar.test.tsx @@ -1,7 +1,6 @@ /** * @jest-environment jsdom */ - import { getAvatar } from './getAvatar'; import { publicClient } from '../../network/client'; @@ -20,7 +19,7 @@ describe('getAvatar', () => { mockGetEnsAvatar.mockResolvedValue(expectedAvatarUrl); - const avatarUrl = await getAvatar(ensName); + const avatarUrl = await getAvatar({ ensName }); expect(avatarUrl).toBe(expectedAvatarUrl); expect(mockGetEnsAvatar).toHaveBeenCalledWith({ name: ensName }); @@ -31,6 +30,6 @@ describe('getAvatar', () => { mockGetEnsAvatar.mockRejectedValue(new Error('This is an error')); - await expect(getAvatar(ensName)).rejects.toThrow('This is an error'); + await expect(getAvatar({ ensName })).rejects.toThrow('This is an error'); }); }); diff --git a/src/identity/core/getAvatar.ts b/src/identity/core/getAvatar.ts index 2ada5ec53f..db3e64d428 100644 --- a/src/identity/core/getAvatar.ts +++ b/src/identity/core/getAvatar.ts @@ -1,9 +1,9 @@ import { normalize } from 'viem/ens'; import { publicClient } from '../../network/client'; -import { GetAvatarReturnType } from '../types'; +import { GetAvatar, GetAvatarReturnType } from '../types'; -export const getAvatar = async (ensName: string): Promise => { +export const getAvatar = async (params: GetAvatar): Promise => { return await publicClient.getEnsAvatar({ - name: normalize(ensName), + name: normalize(params.ensName), }); }; diff --git a/src/identity/hooks/useAvatar.ts b/src/identity/hooks/useAvatar.ts index 2bd22d62db..71af493077 100644 --- a/src/identity/hooks/useAvatar.ts +++ b/src/identity/hooks/useAvatar.ts @@ -20,7 +20,7 @@ export const useAvatar = ({ ensName }: UseAvatarOptions, queryOptions?: UseAvata return useQuery({ queryKey: ['useAvatar', ensActionKey], queryFn: async () => { - return await getAvatar(ensName); + return getAvatar({ ensName }); }, gcTime: cacheTime, enabled, diff --git a/src/identity/index.ts b/src/identity/index.ts index c4cce28dbb..5631195c68 100644 --- a/src/identity/index.ts +++ b/src/identity/index.ts @@ -15,6 +15,7 @@ export type { EASSchemaUid, EASChainDefinition, GetAttestationsOptions, + GetAvatar, GetAvatarReturnType, GetNameReturnType, } from './types'; diff --git a/src/identity/types.ts b/src/identity/types.ts index f34214afce..a59c29acbe 100644 --- a/src/identity/types.ts +++ b/src/identity/types.ts @@ -72,6 +72,13 @@ export type GetAttestationsOptions = { limit?: number; }; +/** + * Note: exported as public Type + */ +export type GetAvatar = { + ensName: string; // The ENS name to fetch the avatar for. +}; + /** * Note: exported as public Type */ @@ -107,3 +114,25 @@ export type UseAttestations = { chain: Chain; schemaId: Address; }; + +export type WithAvatarBadgeInnerReact = { + children: React.ReactNode; + address: Address; +}; + +export type WithAvatarBadgeReact = { + children: React.ReactNode; + showAttestation: boolean; + address: Address; +}; + +export type WithNameBadgeInnerReact = { + children: React.ReactNode; + address: Address; +}; + +export type WithNameBadgeReact = { + children: React.ReactNode; + showAttestation?: boolean; + address: Address; +}; diff --git a/src/index.css b/src/index.css index 56740ada68..904dd47b7b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,2 +1,11 @@ +@import url('./identity/components/Avatar.css'); +@import url('./identity/components/Badge.css'); +@import url('./identity/components/WithAvatarBadge.css'); +@import url('./identity/components/WithNameBadge.css'); @import url('./token/components/TextInput.css'); @import url('./token/components/TokenChip.css'); +@import url('./token/components/TokenImage.css'); +@import url('./token/components/TokenRow.css'); +@import url('./token/components/TokenSelector.css'); +@import url('./token/components/TokenSelectorDropdown.css'); +@import url('./wallet/components/ConnectAccount.css'); diff --git a/src/isBase.ts b/src/isBase.ts index 81147362fd..7415c18ad0 100644 --- a/src/isBase.ts +++ b/src/isBase.ts @@ -1,5 +1,5 @@ -import { isBaseOptions } from './types'; import { baseSepolia, base } from 'viem/chains'; +import type { isBaseOptions } from './types'; /** * isBase diff --git a/src/queries/attestations.test.ts b/src/queries/attestations.test.ts index fe76cf145d..2c44331ee1 100644 --- a/src/queries/attestations.test.ts +++ b/src/queries/attestations.test.ts @@ -1,5 +1,5 @@ import { - GetAttestationsByFilterOptions, + type GetAttestationsByFilterOptions, getAttestationQueryVariables, getAttestationsByFilter, attestationQuery, diff --git a/src/queries/attestations.ts b/src/queries/attestations.ts index 9d118ab298..3737aea492 100644 --- a/src/queries/attestations.ts +++ b/src/queries/attestations.ts @@ -1,8 +1,8 @@ import { gql } from 'graphql-request'; -import type { Address, Chain } from 'viem'; import { getAddress } from 'viem'; -import { EASSchemaUid, Attestation } from '../identity/types'; import { createEasGraphQLClient } from '../network/createEasGraphQLClient'; +import type { Address, Chain } from 'viem'; +import type { EASSchemaUid, Attestation } from '../identity/types'; /** * Type representing the filter options used for querying EAS Attestations. diff --git a/src/queries/request.test.ts b/src/queries/request.test.ts index 6b80bb6fe3..bdcf2c700f 100644 --- a/src/queries/request.test.ts +++ b/src/queries/request.test.ts @@ -1,5 +1,4 @@ import { setOnchainKitConfig } from '../OnchainKitConfig'; -import { version } from '../version'; import { buildRequestBody, sendRequest } from './request'; describe('request', () => { @@ -47,7 +46,6 @@ describe('request', () => { body: JSON.stringify(requestBody), headers: { 'Content-Type': 'application/json', - onchainkit_version: version, }, }); expect(response).toEqual(mockResponse); diff --git a/src/queries/request.ts b/src/queries/request.ts index e042633d42..135106bf58 100644 --- a/src/queries/request.ts +++ b/src/queries/request.ts @@ -23,7 +23,6 @@ export type JSONRPCResult = { const POST_METHOD = 'POST'; const JSON_HEADERS = { 'Content-Type': 'application/json', - onchainkit_version: version, }; const JSON_RPC_VERSION = '2.0'; diff --git a/src/swap/core/formatDecimals.test.ts b/src/swap/core/formatDecimals.test.ts new file mode 100644 index 0000000000..7ee9463e1c --- /dev/null +++ b/src/swap/core/formatDecimals.test.ts @@ -0,0 +1,63 @@ +import { formatDecimals } from './formatDecimals'; + +describe('formatDecimals', () => { + it('should format the amount correctly with default decimals when inputInDecimals is true', () => { + const amount = '1500000000000000000'; + const expectedFormattedAmount = '1.5'; + const result = formatDecimals(amount, true, 18); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with custom decimals when inputInDecimals is true', () => { + const amount = '1500000000000000000'; + const decimals = 9; + const expectedFormattedAmount = '1500000000'; + const result = formatDecimals(amount, true, decimals); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with default decimals when inputInDecimals is false', () => { + const amount = '1.5'; + const expectedFormattedAmount = '1500000000000000000'; + const result = formatDecimals(amount, false, 18); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with custom decimals when inputInDecimals is false', () => { + const amount = '1.5'; + const decimals = 9; + const expectedFormattedAmount = '1500000000'; + const result = formatDecimals(amount, false, decimals); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with default decimals when inputInDecimals is true and decimals is not provided', () => { + const amount = '1500000000000000000'; + const expectedFormattedAmount = '1.5'; + const result = formatDecimals(amount); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with default decimals when inputInDecimals is true and decimals is provided', () => { + const amount = '1500000000'; + const expectedFormattedAmount = '1.5'; + const decimals = 9; + const result = formatDecimals(amount, true, decimals); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with default decimals when inputInDecimals is false and decimals is provided', () => { + const amount = '1.5'; + const expectedFormattedAmount = '1500000000'; + const decimals = 9; + const result = formatDecimals(amount, false, decimals); + expect(result).toEqual(expectedFormattedAmount); + }); + + it('should format the amount correctly with default decimals when inputInDecimals is false and decimals is not provided', () => { + const amount = '1.5'; + const expectedFormattedAmount = '1.5e-18'; + const result = formatDecimals(amount); + expect(result).toEqual(expectedFormattedAmount); + }); +}); diff --git a/src/swap/core/formatDecimals.ts b/src/swap/core/formatDecimals.ts new file mode 100644 index 0000000000..0b4e4f74f9 --- /dev/null +++ b/src/swap/core/formatDecimals.ts @@ -0,0 +1,9 @@ +/** + * Formats an amount according to the decimals. Defaults to 18 decimals for ERC-20s. + */ +export function formatDecimals(amount: string, inputInDecimals = true, decimals = 18): string { + if (inputInDecimals) { + return (Number(amount) / 10 ** decimals).toString(); + } + return (Number(amount) * 10 ** decimals).toString(); +} diff --git a/src/swap/core/getParamsForToken.test.ts b/src/swap/core/getParamsForToken.test.ts new file mode 100644 index 0000000000..19c9eecb50 --- /dev/null +++ b/src/swap/core/getParamsForToken.test.ts @@ -0,0 +1,150 @@ +import type { Token } from '../../token'; +import { getParamsForToken } from './getParamsForToken'; + +describe('getParamsForToken', () => { + it('should return the correct GetQuoteAPIParams object', () => { + const from: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }; + const to: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }; + const amount = '1.5'; + const amountReference = 'from'; + + const expectedParams = { + from: 'ETH', + to: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + amount: '1500000000000000000', + amountReference: 'from', + }; + + const result = getParamsForToken({ + from, + to, + amount, + amountReference, + }); + + expect(result).toEqual(expectedParams); + }); + + it('should use the default for amountReference', () => { + const from: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }; + const to: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }; + const amount = '1.5'; + + const expectedParams = { + from: 'ETH', + to: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + amount: '1500000000000000000', + amountReference: 'from', + }; + + const result = getParamsForToken({ + from, + to, + amount, + }); + + expect(result).toEqual(expectedParams); + }); + + it('should format the amount correctly with default decimals when isAmountInDecimals is true', () => { + const to: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }; + const from: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }; + const amount = '1500000000000000000'; + const amountReference = 'from'; + const isAmountInDecimals = true; + const expectedParams = { + from: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + to: 'ETH', + amount: '1500000000000000000', + amountReference: 'from', + }; + const result = getParamsForToken({ + from, + to, + amount, + amountReference, + isAmountInDecimals, + }); + expect(result).toEqual(expectedParams); + }); + + it('should format the amount correctly with default decimals when isAmountInDecimals is false', () => { + const to: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }; + const from: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }; + const amount = '1.5'; + const amountReference = 'from'; + const expectedParams = { + from: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + to: 'ETH', + amount: '1500000000000000000', + amountReference: 'from', + }; + const result = getParamsForToken({ + from, + to, + amount, + amountReference, + }); + expect(result).toEqual(expectedParams); + }); +}); diff --git a/src/swap/core/getParamsForToken.ts b/src/swap/core/getParamsForToken.ts new file mode 100644 index 0000000000..b7e567f25d --- /dev/null +++ b/src/swap/core/getParamsForToken.ts @@ -0,0 +1,18 @@ +import { formatDecimals } from './formatDecimals'; +import type { GetQuoteParams, GetQuoteAPIParams } from '../types'; + +/** + * Converts parameters with `Token` to ones with address. Additionally adds default values for optional request fields. + */ +export function getParamsForToken(params: GetQuoteParams): GetQuoteAPIParams { + const { from, to, amount, amountReference, isAmountInDecimals } = params; + + const decimals = amountReference === 'from' ? from.decimals : to.decimals; + + return { + from: from.address || 'ETH', + to: to.address || 'ETH', + amount: isAmountInDecimals ? amount : formatDecimals(amount, false, decimals), + amountReference: amountReference || 'from', + }; +} diff --git a/src/swap/core/getQuote.test.ts b/src/swap/core/getQuote.test.ts new file mode 100644 index 0000000000..0afcc63863 --- /dev/null +++ b/src/swap/core/getQuote.test.ts @@ -0,0 +1,131 @@ +import { getQuote } from './getQuote'; +import { sendRequest } from '../../queries/request'; +import { CDP_GETSWAPQUOTE } from '../../definitions/swap'; +import type { Token } from '../../token/types'; +import { getParamsForToken } from './getParamsForToken'; + +jest.mock('../../queries/request'); + +const ETH: Token = { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, +}; +const DEGEN: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, +}; +const testAmount = '3305894409732200'; +const testAmountReference = 'from'; + +describe('getQuote', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return a quote for a swap', async () => { + const mockParams = { + amountReference: testAmountReference, + from: ETH, + to: DEGEN, + amount: testAmount, + }; + const mockApiParams = getParamsForToken(mockParams); + + const mockResponse = { + id: 1, + jsonrpc: '2.0', + result: { + from: { + address: '', + chainId: 8453, + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + name: 'ETH', + symbol: 'ETH', + }, + to: { + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + chainId: 8453, + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + name: 'DEGEN', + symbol: 'DEGEN', + }, + fromAmount: '100000000000000000', + toAmount: '16732157880511600003860', + amountReference: 'from', + priceImpact: '0.07', + chainId: 8453, + hasHighPriceImpact: false, + slippage: '3', + }, + }; + + (sendRequest as jest.Mock).mockResolvedValue(mockResponse); + + const quote = await getQuote(mockParams); + + expect(quote).toEqual(mockResponse.result); + + expect(sendRequest).toHaveBeenCalledTimes(1); + expect(sendRequest).toHaveBeenCalledWith(CDP_GETSWAPQUOTE, [mockApiParams]); + }); + + it('should throw an error if sendRequest fails', async () => { + const mockParams = { + amountReference: testAmountReference, + from: ETH, + to: DEGEN, + amount: testAmount, + }; + const mockApiParams = getParamsForToken(mockParams); + + const mockError = new Error('getQuote: Error: Failed to send request'); + (sendRequest as jest.Mock).mockRejectedValue(mockError); + + await expect(getQuote(mockParams)).rejects.toThrow('getQuote: Error: Failed to send request'); + + expect(sendRequest).toHaveBeenCalledTimes(1); + expect(sendRequest).toHaveBeenCalledWith(CDP_GETSWAPQUOTE, [mockApiParams]); + }); + + it('should return an error object from getQuote', async () => { + const mockParams = { + amountReference: testAmountReference, + from: ETH, + to: DEGEN, + amount: testAmount, + }; + const mockApiParams = getParamsForToken(mockParams); + + const mockResponse = { + id: 1, + jsonrpc: '2.0', + error: { + code: -1, + message: 'Invalid response', + }, + }; + + (sendRequest as jest.Mock).mockResolvedValue(mockResponse); + + const error = await getQuote(mockParams); + expect(error).toEqual({ + code: -1, + error: 'Invalid response', + }); + + expect(sendRequest).toHaveBeenCalledTimes(1); + expect(sendRequest).toHaveBeenCalledWith(CDP_GETSWAPQUOTE, [mockApiParams]); + }); +}); diff --git a/src/swap/core/getQuote.ts b/src/swap/core/getQuote.ts new file mode 100644 index 0000000000..facb088389 --- /dev/null +++ b/src/swap/core/getQuote.ts @@ -0,0 +1,38 @@ +import { CDP_GETSWAPQUOTE } from '../../definitions/swap'; +import { sendRequest } from '../../queries/request'; +import type { + GetQuoteResponse, + GetQuoteParams, + GetQuoteAPIParams, + Quote, + SwapError, +} from '../types'; +import { getParamsForToken } from './getParamsForToken'; + +/** + * Retrieves a quote for a swap from Token A to Token B. + */ +export async function getQuote(params: GetQuoteParams): Promise { + // Default parameters + const defaultParams = { + amountReference: 'from', + isAmountInDecimals: false, + }; + + const apiParams = getParamsForToken({ ...defaultParams, ...params }); + + try { + const res = await sendRequest(CDP_GETSWAPQUOTE, [apiParams]); + + if (res.error) { + return { + code: res.error.code, + error: res.error.message, + } as SwapError; + } + + return res.result; + } catch (error) { + throw new Error(`getQuote: ${error}`); + } +} diff --git a/src/swap/index.ts b/src/swap/index.ts index d8b8c70a0d..e51725e16b 100644 --- a/src/swap/index.ts +++ b/src/swap/index.ts @@ -1 +1,10 @@ // 🌲☀️🌲 +export { getQuote } from './core/getQuote'; +export type { + Fee, + GetQuoteParams, + GetQuoteResponse, + Quote, + QuoteWarning, + SwapError, +} from './types'; diff --git a/src/swap/types.ts b/src/swap/types.ts index ddf5cb8cb5..5e73f0365b 100644 --- a/src/swap/types.ts +++ b/src/swap/types.ts @@ -1,49 +1,84 @@ -import { Address } from 'viem'; -import { Token } from '../token/types'; +import type { Address } from 'viem'; +import type { Token } from '../token/types'; export type AddressOrETH = Address | 'ETH'; +/** + * Note: exported as public Type + */ export type Fee = { - amount: string; - baseAsset: Token; - percentage: string; + amount: string; // The amount of the fee + baseAsset: Token; // The base asset for the fee + percentage: string; // The percentage of the fee }; +export type GetQuoteAPIParams = { + from: AddressOrETH | ''; // The source address or 'ETH' for Ethereum + to: AddressOrETH | ''; // The destination address or 'ETH' for Ethereum + amount: string; // The amount to be swapped + amountReference?: string; // The reference amount for the swap +}; + +/** + * Note: exported as public Type + */ +export type GetQuoteParams = { + from: Token; // The source token for the swap + to: Token; // The destination token for the swap + amount: string; // The amount to be swapped + amountReference?: string; // The reference amount for the swap + isAmountInDecimals?: boolean; // Whether the amount is in decimals +}; + +/** + * Note: exported as public Type + */ +export type GetQuoteResponse = Quote | SwapError; + +/** + * Note: exported as public Type + */ export type Quote = { - amountReference: string; - fromAmount: string; - fromAsset: Token; - highPriceImpact: boolean; - priceImpact: string; - slippage: string; - toAmount: string; - toAsset: Token; - warning?: QuoteWarning; + amountReference: string; // The reference amount for the quote + from: Token; // The source token for the swap + fromAmount: string; // The amount of the source token + hasHighPriceImpact: boolean; // Whether the price impact is high + priceImpact: string; // The price impact of the swap + slippage: string; // The slippage of the swap + to: Token; // The destination token for the swap + toAmount: string; // The amount of the destination token + warning?: QuoteWarning; // The warning associated with the quote }; +/** + * Note: exported as public Type + */ export type QuoteWarning = { - description?: string; - message?: string; - type?: string; + description?: string; // The description of the warning + message?: string; // The message of the warning + type?: string; // The type of the warning }; -export type Trade = { - approveTx?: Transaction; - chainId: string; - fee: Fee; - tx: Transaction; +/** + * Note: exported as public Type + */ +export type SwapError = { + code: number; // The error code + error: string; // The error message }; -export type Transaction = { - data: string; - from: string; - gas: string; - gasPrice: string; - to: string; - value: string; +export type Trade = { + approveTx?: Transaction; // The approval transaction + chainId: string; // The chain ID + fee: Fee; // The fee for the trade + tx: Transaction; // The trade transaction }; -export type TrendingToken = Token & { - numOfBuys: number; - numOfSells: number; +export type Transaction = { + data: string; // The transaction data + from: string; // The sender address + gas: string; // The gas limit + gasPrice: string; // The gas price + to: string; // The recipient address + value: string; // The value of the transaction }; diff --git a/src/token/components/TextInput.css b/src/token/components/TextInput.css index f12da931ca..ebd147dbb4 100644 --- a/src/token/components/TextInput.css +++ b/src/token/components/TextInput.css @@ -1,31 +1,24 @@ .ock-textinput-container { @apply relative flex items-center; } - .ock-textinput-iconsearch { @apply absolute left-4 top-2/4 -translate-y-2/4; } - .ock-textinput-input { @apply w-full rounded-full border-2 border-solid border-[#eef0f3] py-2 pl-12 pr-5 text-[#0A0B0D]; background: #eef0f3; + outline: none; &::placeholder { @apply text-[#5B616E]; } &:hover { - @apply border-[#cacbce]; background: #cacbce; } - &:focus { - @apply border-[#0052FF]; - outline: none; - } &:hover:focus { background: #eef0f3; } } - .ock-textinput-clearbutton { @apply absolute right-4 top-2/4 -translate-y-2/4; } diff --git a/src/token/components/TokenChip.css b/src/token/components/TokenChip.css index bda3edfc7c..e65dadaac1 100644 --- a/src/token/components/TokenChip.css +++ b/src/token/components/TokenChip.css @@ -5,16 +5,13 @@ &:hover { background: #cacbce; } - &:active { background: #bfc1c3; } } - .ock-tokenchip-label { @apply text-base font-medium leading-4 text-black; } - .ock-tokenchip-image { @apply mr-2 h-6 w-6; } diff --git a/src/token/components/TokenImage.css b/src/token/components/TokenImage.css new file mode 100644 index 0000000000..5525f98c3c --- /dev/null +++ b/src/token/components/TokenImage.css @@ -0,0 +1,3 @@ +.ock-tokenimage { + @apply overflow-hidden rounded-[50%]; +} diff --git a/src/token/components/TokenImage.test.tsx b/src/token/components/TokenImage.test.tsx index 1494bc3539..a268ec5ff5 100644 --- a/src/token/components/TokenImage.test.tsx +++ b/src/token/components/TokenImage.test.tsx @@ -5,14 +5,33 @@ import React from 'react'; import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { TokenImage } from './TokenImage'; +import { Token } from '../types'; + +const tokenWithImage: Token = { + name: 'Ethereum', + address: '0x123', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, +}; + +const tokenWithNoImage: Token = { + name: 'Ethereum', + address: '0x123', + symbol: 'ETH', + decimals: 18, + image: null, + chainId: 8453, +}; describe('TokenImage Component', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should render with src prop', async () => { - render(); + it('should render token with image', async () => { + render(); const imgElement = screen.getByTestId('ockTokenImage_Image'); const noImgElement = screen.queryByTestId('ockTokenImage_NoImage'); @@ -20,8 +39,8 @@ describe('TokenImage Component', () => { expect(noImgElement).toBeNull(); }); - it('should render with no src prop', async () => { - render(); + it('should render token with no image', async () => { + render(); const imgElement = screen.queryByTestId('ockTokenImage_Image'); const noImgElement = screen.getByTestId('ockTokenImage_NoImage'); @@ -30,7 +49,7 @@ describe('TokenImage Component', () => { }); it('should render with size prop', async () => { - render(); + render(); const imgElement = screen.queryByTestId('ockTokenImage_Image'); const noImgElement = screen.getByTestId('ockTokenImage_NoImage'); diff --git a/src/token/components/TokenImage.tsx b/src/token/components/TokenImage.tsx index 075fde97c9..632f2ed970 100644 --- a/src/token/components/TokenImage.tsx +++ b/src/token/components/TokenImage.tsx @@ -1,31 +1,42 @@ import { useMemo } from 'react'; import { TokenImageReact } from '../types'; +import { getTokenImageColor } from './getTokenImageColor'; + +export function TokenImage({ className, size = 24, token }: TokenImageReact) { + const { image, name } = token; -export function TokenImage({ src, size = 24 }: TokenImageReact) { const styles = useMemo(() => { return { image: { width: `${size}px`, height: `${size}px`, - borderRadius: '50%', - overflow: 'hidden', - background: 'blue', }, placeholderImage: { - background: 'blue', + background: getTokenImageColor(name), width: `${size}px`, height: `${size}px`, }, }; }, [size]); - if (!src) { + if (!image) { return ( -
+
); } - return ; + return ( + + ); } diff --git a/src/token/components/TokenRow.css b/src/token/components/TokenRow.css new file mode 100644 index 0000000000..bb457f6f3c --- /dev/null +++ b/src/token/components/TokenRow.css @@ -0,0 +1,26 @@ +.ock-tokenrow-button { + @apply flex h-16 w-full cursor-pointer items-center justify-between px-2 py-1; + background: #ffffff; + + &:hover { + background: #cacbce; + } + &:active { + background: #bfc1c3; + } +} +.ock-tokenrow-left { + @apply flex items-center gap-3; +} +.ock-tokenrow-body { + @apply flex flex-col items-start; +} +.ock-tokenrow-name { + @apply text-base font-medium leading-normal text-[#0A0B0D]; +} +.ock-tokenrow-symbol { + @apply text-base font-normal leading-normal text-[#5B616E]; +} +.ock-tokenrow-data { + @apply text-base font-normal leading-normal text-[#5B616E]; +} diff --git a/src/token/components/TokenRow.test.tsx b/src/token/components/TokenRow.test.tsx index e01a86f37a..7d0bfbb04b 100644 --- a/src/token/components/TokenRow.test.tsx +++ b/src/token/components/TokenRow.test.tsx @@ -27,7 +27,7 @@ describe('TokenRow component', () => { render(); await waitFor(() => { - const circle = screen.getByTestId('ockTokenRow_PlaceholderImage'); + const circle = screen.getByTestId('ockTokenImage_NoImage'); expect(circle).toBeInTheDocument(); }); }); @@ -36,7 +36,7 @@ describe('TokenRow component', () => { render(); await waitFor(() => { - const tokenImage = screen.getByTestId('ockTokenRow_Image'); + const tokenImage = screen.getByTestId('ockTokenImage_Image'); expect(tokenImage).toBeInTheDocument(); }); }); diff --git a/src/token/components/TokenRow.tsx b/src/token/components/TokenRow.tsx index 6a342cc809..10b401811c 100644 --- a/src/token/components/TokenRow.tsx +++ b/src/token/components/TokenRow.tsx @@ -1,66 +1,29 @@ -import { memo, CSSProperties } from 'react'; +import { memo } from 'react'; import { TokenRowReact } from '../types'; import { formatAmount } from '../core/formatAmount'; +import { TokenImage } from './TokenImage'; -const styles = { - row: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - cursor: 'pointer', - padding: '4px 8px', - width: '100%', - }, - left: { - display: 'flex', - alignItems: 'center', - }, - image: { - height: '32px', - width: '32px', - marginRight: '12px', - }, - circle: { - height: '32px', - width: '32px', - borderRadius: '99999px', - background: 'gray', - marginRight: '12px', - }, - column: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - }, - label1: { - fontSize: '16px', - lineHeight: '1.5', - fontWeight: 500, - color: '#0A0B0D', - }, - label2: { - fontSize: '16px', - lineHeight: '1.5', - fontWeight: 400, - color: '#5B616E', - }, -} as Record; - -export const TokenRow = memo(function TokenRow({ token, amount, onClick }: TokenRowReact) { +export const TokenRow = memo(function TokenRow({ + token, + amount, + onClick, + hideImage, + hideSymbol, +}: TokenRowReact) { return ( - +
+ + {isOpen && + isValidElement(children) && + cloneElement(children, { + onToggle: handleToggle, + })} +
); } diff --git a/src/token/components/TokenSelectorDropdown.css b/src/token/components/TokenSelectorDropdown.css new file mode 100644 index 0000000000..404353c34f --- /dev/null +++ b/src/token/components/TokenSelectorDropdown.css @@ -0,0 +1,9 @@ +.ock-tokenselectordropdown-container { + @apply absolute z-10 mt-1 flex max-h-80 w-[250px] flex-col overflow-y-hidden rounded-lg; + scrollbar-width: thin; + scrollbar-color: #d1d5db #ffffff; +} + +.ock-tokenselectordropdown-scroll { + @apply overflow-y-auto; +} diff --git a/src/token/components/TokenSelectorDropdown.test.tsx b/src/token/components/TokenSelectorDropdown.test.tsx new file mode 100644 index 0000000000..b81274ae19 --- /dev/null +++ b/src/token/components/TokenSelectorDropdown.test.tsx @@ -0,0 +1,60 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Address } from 'viem'; +import { TokenSelectorDropdown } from './TokenSelectorDropdown'; +import { Token } from '../types'; + +describe('TokenSelectorDropdown', () => { + const setToken = jest.fn(); + const onToggle = jest.fn(); + const options: Token[] = [ + { + name: 'Ethereum', + address: '' as Address, + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, + { + name: 'USDC', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' as Address, + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + }, + ]; + + it('renders the TokenSelectorDropdown component', () => { + render(); + + const result = screen.getAllByTestId('ockTokenRow_Container'); + expect(result.length).toEqual(options.length); + }); + + it('calls setToken and onToggle when clicking on a token', async () => { + render(); + + await waitFor(() => { + fireEvent.click(screen.getByText(options[0].name)); + + expect(setToken).toHaveBeenCalledWith(options[0]); + expect(onToggle).toHaveBeenCalled(); + }); + }); + + it('calls onToggle when clicking outside the component', async () => { + render(); + + await waitFor(() => { + fireEvent.click(document); + + expect(onToggle).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/token/components/TokenSelectorDropdown.tsx b/src/token/components/TokenSelectorDropdown.tsx new file mode 100644 index 0000000000..b481f43220 --- /dev/null +++ b/src/token/components/TokenSelectorDropdown.tsx @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { TokenSelectorDropdownReact } from '../types'; +import { TokenRow } from './TokenRow'; + +export function TokenSelectorDropdown({ setToken, options, onToggle }: TokenSelectorDropdownReact) { + const ref = useRef(null); + + const handleBlur = useCallback((event: MouseEvent) => { + /* istanbul ignore next */ + if (ref.current && !ref.current.contains(event.target as Node)) { + onToggle(); + } + }, []); + + useEffect(() => { + // NOTE: this ensures that handleBlur doesn't get called on initial mount + // We need to use non-div elements to properly handle onblur events + setTimeout(() => { + document.addEventListener('click', handleBlur); + }, 0); + + return () => { + document.removeEventListener('click', handleBlur); + }; + }, []); + + return ( +
+
+ {options.map((token) => ( + { + setToken(token); + onToggle(); + }} + /> + ))} +
+
+ ); +} diff --git a/src/token/components/getTokenImageColor.test.ts b/src/token/components/getTokenImageColor.test.ts new file mode 100644 index 0000000000..162151f3ba --- /dev/null +++ b/src/token/components/getTokenImageColor.test.ts @@ -0,0 +1,16 @@ +import { getTokenImageColor } from './getTokenImageColor'; + +describe('getTokenImageColor', () => { + it('should return a consistent color for the same string', () => { + const str = 'test'; + const color1 = getTokenImageColor(str); + const color2 = getTokenImageColor(str); + expect(color1).toBe(color2); + }); + + it('should return different colors for different strings', () => { + const color1 = getTokenImageColor('test1'); + const color2 = getTokenImageColor('test2'); + expect(color1).not.toBe(color2); + }); +}); diff --git a/src/token/components/getTokenImageColor.ts b/src/token/components/getTokenImageColor.ts new file mode 100644 index 0000000000..9693d28613 --- /dev/null +++ b/src/token/components/getTokenImageColor.ts @@ -0,0 +1,19 @@ +function hashStringToNumber(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; +} + +function numberToRGB(hash: number) { + const h = Math.abs(hash) % 360; + const s = (Math.abs(hash >> 8) % 31) + 50; + const l = (Math.abs(hash >> 16) % 21) + 40; + return `hsl(${h}, ${s}%, ${l}%)`; +} + +export function getTokenImageColor(str: string) { + const hash = hashStringToNumber(`${str}`); + return numberToRGB(hash); +} diff --git a/src/token/core/getToken.test.ts b/src/token/core/getToken.test.ts index 7bc65f18bd..55ea4f822e 100644 --- a/src/token/core/getToken.test.ts +++ b/src/token/core/getToken.test.ts @@ -1,6 +1,6 @@ import { getTokens } from './getTokens'; import { sendRequest } from '../../queries/request'; -import { ListSwapAssets } from '../../definitions/swap'; +import { CDP_LISTSWAPASSETS } from '../../definitions/swap'; jest.mock('../../queries/request'); @@ -16,17 +16,17 @@ describe('getTokens', () => { address: '0x123', chainId: 1, decimals: 18, - imageURL: 'https://example.com/token.png', + image: 'https://example.com/token.png', name: 'Token 1', - currencyCode: 'TKN1', + symbol: 'TKN1', }, { address: '0x456', chainId: 1, decimals: 18, - imageURL: 'https://example.com/token.png', + image: 'https://example.com/token.png', name: 'Token 2', - currencyCode: 'TKN2', + symbol: 'TKN2', }, ], error: null, @@ -56,7 +56,7 @@ describe('getTokens', () => { ]); expect(sendRequest).toHaveBeenCalledTimes(1); - expect(sendRequest).toHaveBeenCalledWith(ListSwapAssets, [{ limit: '50', page: '1' }]); + expect(sendRequest).toHaveBeenCalledWith(CDP_LISTSWAPASSETS, [{ limit: '50', page: '1' }]); }); it('should accept options parameters', async () => { @@ -66,9 +66,9 @@ describe('getTokens', () => { address: '0x123', chainId: 1, decimals: 18, - imageURL: 'https://example.com/token.png', + image: 'https://example.com/token.png', name: 'Token 1', - currencyCode: 'TKN1', + symbol: 'TKN1', }, ], error: null, @@ -90,7 +90,7 @@ describe('getTokens', () => { ]); expect(sendRequest).toHaveBeenCalledTimes(1); - expect(sendRequest).toHaveBeenCalledWith(ListSwapAssets, [{ limit: '1', page: '1' }]); + expect(sendRequest).toHaveBeenCalledWith(CDP_LISTSWAPASSETS, [{ limit: '1', page: '1' }]); }); it('should return an error object if sendRequest returns an error', async () => { @@ -113,7 +113,7 @@ describe('getTokens', () => { }); expect(sendRequest).toHaveBeenCalledTimes(1); - expect(sendRequest).toHaveBeenCalledWith(ListSwapAssets, [{ limit: '50', page: '1' }]); + expect(sendRequest).toHaveBeenCalledWith(CDP_LISTSWAPASSETS, [{ limit: '50', page: '1' }]); }); it('should rethrow the error if an error occurs during token retrieval', async () => { @@ -126,6 +126,6 @@ describe('getTokens', () => { ); expect(sendRequest).toHaveBeenCalledTimes(1); - expect(sendRequest).toHaveBeenCalledWith(ListSwapAssets, [{ limit: '50', page: '1' }]); + expect(sendRequest).toHaveBeenCalledWith(CDP_LISTSWAPASSETS, [{ limit: '50', page: '1' }]); }); }); diff --git a/src/token/core/getTokens.ts b/src/token/core/getTokens.ts index a2defb2ce3..b9e49e008b 100644 --- a/src/token/core/getTokens.ts +++ b/src/token/core/getTokens.ts @@ -1,5 +1,5 @@ -import { ListSwapAssets } from '../../definitions/swap'; -import { LegacyTokenData, GetTokensOptions, GetTokensResponse, GetTokensError } from '../types'; +import { CDP_LISTSWAPASSETS } from '../../definitions/swap'; +import type { Token, GetTokensOptions, GetTokensResponse, GetTokensError } from '../types'; import { sendRequest } from '../../queries/request'; /** @@ -15,7 +15,7 @@ export async function getTokens(options?: GetTokensOptions): Promise(ListSwapAssets, [filters]); + const res = await sendRequest(CDP_LISTSWAPASSETS, [filters]); if (res.error) { return { @@ -24,15 +24,7 @@ export async function getTokens(options?: GetTokensOptions): Promise ({ - address: token.address, - chainId: token.chainId, - decimals: token.decimals, - image: token.imageURL, - name: token.name, - symbol: token.currencyCode, - })); + return res.result; } catch (error) { throw new Error(`getTokens: ${error}`); } diff --git a/src/token/index.ts b/src/token/index.ts index 6cbaba8c59..5bbd8e11e7 100644 --- a/src/token/index.ts +++ b/src/token/index.ts @@ -4,6 +4,7 @@ export { TokenImage } from './components/TokenImage'; export { TokenRow } from './components/TokenRow'; export { TokenSearch } from './components/TokenSearch'; export { TokenSelector } from './components/TokenSelector'; +export { TokenSelectorDropdown } from './components/TokenSelectorDropdown'; export { formatAmount } from './core/formatAmount'; export { getTokens } from './core/getTokens'; export { TokenAmountInput } from './components/TokenAmountInput'; diff --git a/src/token/types.ts b/src/token/types.ts index 29d87cec85..9ad11e61c1 100644 --- a/src/token/types.ts +++ b/src/token/types.ts @@ -1,21 +1,7 @@ // 🌲☀️🌲 +import { ReactElement } from 'react'; import { Address } from 'viem'; -// The raw response from the Swap API -// Contains legacy fields that have been renamed in the OnchainKit Token type -export type LegacyTokenData = { - name: string; - address: Address; - currencyCode: string; - decimals: number; - imageURL: string; - blockchain: string; - aggregators: string[]; - swappable: boolean; - unverified: boolean; - chainId: number; -}; - /** * Note: exported as public Type */ @@ -56,7 +42,7 @@ export type GetTokensOptions = { * Note: exported as public Type */ export type Token = { - address: Address; // The address of the token contract + address: Address | ''; // The address of the token contract, this value will be empty for native ETH chainId: number; // The chain id of the token contract decimals: number; // The number of token decimals image: string | null; // A string url of the token logo @@ -76,8 +62,9 @@ export type TokenChipReact = { * Note: exported as public Type */ export type TokenImageReact = { - src: string | null; + className?: string; // Optional additional CSS class to apply to the component size?: number; + token: Token; }; /** @@ -87,6 +74,8 @@ export type TokenRowReact = { token: Token; amount?: string; onClick?: (token: Token) => void; + hideSymbol?: boolean; + hideImage?: boolean; }; /** @@ -101,8 +90,18 @@ export type TokenSearchReact = { * Note: exported as public Type */ export type TokenSelectorReact = { + children: ReactElement<{ onToggle: () => void }>; + setToken: (token: Token) => void; token?: Token; - onClick: () => void; +}; + +/** + * Note: exported as public Type + */ +export type TokenSelectorDropdownReact = { + onToggle: () => void; // Injected by TokenSelector + options: Token[]; // List of tokens + setToken: (token: Token) => void; // Token setter }; export type TokenAmountInputReact = { diff --git a/src/types.ts b/src/types.ts index ab139511e0..7509d37f89 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; -import { Address, Chain } from 'viem'; -import { EASSchemaUid } from './identity/types'; +import type { ReactNode } from 'react'; +import type { Address, Chain } from 'viem'; +import type { EASSchemaUid } from './identity/types'; /** * Note: exported as public Type diff --git a/src/version.ts b/src/version.ts index 0b5598fe38..f5aa2dc103 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '0.18.6'; +export const version = '0.19.7'; diff --git a/src/wallet/components/ConnectAccount.css b/src/wallet/components/ConnectAccount.css new file mode 100644 index 0000000000..8a1cd600ed --- /dev/null +++ b/src/wallet/components/ConnectAccount.css @@ -0,0 +1,9 @@ +.ock-connectaccount { + @apply flex flex-grow; +} +.ock-connectaccount-button { + @apply inline-flex h-10 grow items-center justify-center gap-2 rounded-3xl bg-white px-4 py-2; +} +.ock-connectaccount-inner { + @apply text-sm font-medium leading-5 text-black; +} diff --git a/src/wallet/components/ConnectAccount.tsx b/src/wallet/components/ConnectAccount.tsx index 08354cdb07..9a7ddac262 100644 --- a/src/wallet/components/ConnectAccount.tsx +++ b/src/wallet/components/ConnectAccount.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { useAccount, useConnect, useDisconnect } from 'wagmi'; -import { ConnectAccountReact } from '../types'; +import type { ConnectAccountReact } from '../types'; /** * ConnectAccount @@ -27,43 +27,21 @@ export function ConnectAccount({ children }: ConnectAccountReact) { }; return ( -
+
{(() => { if (status === 'disconnected' || status === 'connecting') { return ( ); } - return ; })()}
diff --git a/src/wallet/isValidAAEntrypoint.ts b/src/wallet/isValidAAEntrypoint.ts index 7c501f4e36..6c836ed367 100644 --- a/src/wallet/isValidAAEntrypoint.ts +++ b/src/wallet/isValidAAEntrypoint.ts @@ -1,5 +1,5 @@ import { ENTRYPOINT_ADDRESS_V06 } from 'permissionless'; -import { IsValidAAEntrypointOptions } from './types'; +import type { IsValidAAEntrypointOptions } from './types'; /** * Verify the Account-Abstraction entrypoint before sponsoring a transaction. diff --git a/src/xmtp/getXmtpFrameMessage.test.ts b/src/xmtp/getXmtpFrameMessage.test.ts index dca5b9593f..057dff35a0 100644 --- a/src/xmtp/getXmtpFrameMessage.test.ts +++ b/src/xmtp/getXmtpFrameMessage.test.ts @@ -1,4 +1,4 @@ -import { XmtpOpenFramesRequest } from '@xmtp/frames-validator'; +import type { XmtpOpenFramesRequest } from '@xmtp/frames-validator'; import { getXmtpFrameMessage } from './getXmtpFrameMessage'; describe('getXmtpFrameMessage', () => { diff --git a/yarn.lock b/yarn.lock index 5f591b5fdd..041a1127bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1583,6 +1583,97 @@ __metadata: languageName: node linkType: hard +"@biomejs/biome@npm:^1.8.0": + version: 1.8.0 + resolution: "@biomejs/biome@npm:1.8.0" + dependencies: + "@biomejs/cli-darwin-arm64": "npm:1.8.0" + "@biomejs/cli-darwin-x64": "npm:1.8.0" + "@biomejs/cli-linux-arm64": "npm:1.8.0" + "@biomejs/cli-linux-arm64-musl": "npm:1.8.0" + "@biomejs/cli-linux-x64": "npm:1.8.0" + "@biomejs/cli-linux-x64-musl": "npm:1.8.0" + "@biomejs/cli-win32-arm64": "npm:1.8.0" + "@biomejs/cli-win32-x64": "npm:1.8.0" + dependenciesMeta: + "@biomejs/cli-darwin-arm64": + optional: true + "@biomejs/cli-darwin-x64": + optional: true + "@biomejs/cli-linux-arm64": + optional: true + "@biomejs/cli-linux-arm64-musl": + optional: true + "@biomejs/cli-linux-x64": + optional: true + "@biomejs/cli-linux-x64-musl": + optional: true + "@biomejs/cli-win32-arm64": + optional: true + "@biomejs/cli-win32-x64": + optional: true + bin: + biome: bin/biome + checksum: 0def7e1bef485cb60038d724dd3c11b9e6aaf2aa91593b76ab73f0d52ed319c56831fb5e7fa5a3618ee719b98a55198c88f2cabd8b32c7e17fac9b86f427a95b + languageName: node + linkType: hard + +"@biomejs/cli-darwin-arm64@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-darwin-arm64@npm:1.8.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-x64@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-darwin-x64@npm:1.8.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64-musl@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-linux-arm64-musl@npm:1.8.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-linux-arm64@npm:1.8.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64-musl@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-linux-x64-musl@npm:1.8.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-linux-x64@npm:1.8.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-win32-arm64@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-win32-arm64@npm:1.8.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-win32-x64@npm:1.8.0": + version: 1.8.0 + resolution: "@biomejs/cli-win32-x64@npm:1.8.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@boost/args@npm:^4.0.1": version: 4.0.1 resolution: "@boost/args@npm:4.0.1" @@ -2026,6 +2117,7 @@ __metadata: version: 0.0.0-use.local resolution: "@coinbase/onchainkit@workspace:." dependencies: + "@biomejs/biome": "npm:^1.8.0" "@changesets/changelog-github": "npm:^0.4.8" "@changesets/cli": "npm:^2.26.2" "@coinbase/wallet-sdk": "npm:^4.0.0"