diff --git a/examples/swaps-api-nextjs-sui/.eslintrc.js b/examples/swaps-api-nextjs-sui/.eslintrc.js new file mode 100644 index 0000000..ea43de2 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/.eslintrc.js @@ -0,0 +1,8 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["eslint-config-examples/index.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/examples/swaps-api-nextjs-sui/.gitignore b/examples/swaps-api-nextjs-sui/.gitignore new file mode 100644 index 0000000..55175ef --- /dev/null +++ b/examples/swaps-api-nextjs-sui/.gitignore @@ -0,0 +1,32 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel diff --git a/examples/swaps-api-nextjs-sui/README.md b/examples/swaps-api-nextjs-sui/README.md new file mode 100644 index 0000000..f05acc9 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/README.md @@ -0,0 +1,643 @@ +# Cross-chain Swaps using the Swing API in Next.js for Sui + +This example is built with: + +- [@swing.xyz/sdk](https://developers.swing.xyz/reference/sdk) +- [@mysten/sui.js](https://github.com/MystenLabs/sui/tree/main/sdk/typescript) +- [@mysten/dapp-kit](https://github.com/MystenLabs/sui/tree/main/apps/dapp-kit) +- [Next.js App Router](https://nextjs.org) +- [Tailwind CSS](https://tailwindcss.com) + +## Demo + +Run locally and open http://localhost:3000 + +## Swing Integration + +> The implementation of Swing's [Cross-chain API](https://developers.swing.xyz/reference/api) and [Platform API](https://developers.swing.xyz/reference/api/platform/a2glq2e1w44ad-project-configuration) can be found in `src/components/Swap.tsx`. + +This example demonstrates how you can perform a cross-chain transaction between the Sui and EVM chains (e.g., Ethereum) using Swing's Cross-Chain and Platform APIs via Swing's SDK. + +In this example, we use `@mysten/dapp-kit` to connect to a user's Sui wallet and a standard EVM wallet provider for EVM chains. We will also demonstrate how to utilize Swing's SDK exported API functions, namely `crossChainAPI` and `platformAPI`, to build out a fully functional cross-chain application. + +The process/steps for performing a SUI to ETH transaction, and vice versa, are as follows: + +- Getting a [quote](https://developers.swing.xyz/reference/api/cross-chain/1169f8cbb6937-request-a-transfer-quote) and selecting the best route +- Sending a [token approval](https://developers.swing.xyz/reference/api/contract-calls/approval) request for ERC20 Tokens. (Optional for native coin routes and non-EVM sources such as Sui) +- Sending a [transaction](https://developers.swing.xyz/reference/api/cross-chain/d83d0d65028dc-send-transfer) + +> Although not essential for performing a swap transaction, providing your users with real-time updates on the transaction's status by polling the [status](https://developers.swing.xyz/reference/api/cross-chain/6b61efd1b798a-transfer-status) can significantly enhance the user experience. + +## Getting started + +To get started with this template, first install the required npm dependencies: + +```bash +yarn install +``` + +Next, launch the development server by running the following command: + +```bash +yarn dev --filter=swaps-api-nextjs-sui +``` + +Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. + +## Initializing Swing's SDK + +Swing's SDK contains two (2) vital objects that are crucial to our API integration. Namely, the `platformAPI` and the `crossChainAPI` objects. These objects are a wrapper for all of Swing's Cross-Chain and Platform APIs removing the need for developers to make API requests using libraries like `fetch` or `axios`. The SDK handles those API requests from behind the scenes. + +We've included all the necessary request and response interfaces in the `src/interfaces` folder to aid development. + +Navigating to our `src/services/requests.ts`, let's start by initializing Swing's SDK in our `SwingServiceAPI` class: + +```typescript +import { SwingSDK } from "@swing.xyz/sdk"; + +export class SwingServiceAPI implements ISwingServiceAPI { + private readonly swingSDK: SwingSDK; + + constructor() { + this.swingSDK = new SwingSDK({ + projectId: "replug", + debug: true, + }); + } +} +``` + +The `SwingSDK` constructor accepts a `projectId` as a mandatory parameter and a few other optional parameters: + +| Property | Example | Description | +| ------------- | ------------ | ---------------------------------------------------------------- | +| `projectId` | `replug` | [Swing Platform project identifier](https://platform.swing.xyz/) | +| `debug` | `true` | Enable verbose logging | +| `environment` | `production` | Set's SwingAPI to operate either on testnet or mainnet chains | +| `analytics` | `false` | Enable analytics and error reporting | + +> You can get your `projectId` by signing up to [Swing!](https://platform.swing.xyz/) + +## Getting a Quote + +To perform a swap between SUI and ETH, we first have to get a quote from Swing's Cross-Chain API. + +URL: [https://swap.prod.swing.xyz/v0/transfer/quote](https://swap.prod.swing.xyz/v0/transfer/quote) + +**Parameters**: + +| Property | Example | Description | +| ------------------ | ------------------------------------------ | ----------------------------------------------------------------------- | +| `tokenAmount` | 1000000000 | Amount of the source token being sent (in MIST for SUI, or wei for ETH) | +| `fromChain` | `sui` | Source Chain slug | +| `fromUserAddress` | 0x6c4d...e1a2 | Sender's wallet address (Sui/EVM format depending on source) | +| `fromTokenAddress` | `0x2::sui::SUI` | Source Token Address or coin type (Sui uses coin type) | +| `tokenSymbol` | `SUI` | Source Token symbol | +| `toTokenAddress` | 0x0000000000000000000000000000000000000000 | Destination Token Address (ETH native address for EVM) | +| `toTokenSymbol` | `ETH` | Destination Token symbol | +| `toChain` | `ethereum` | Destination Chain slug | +| `toUserAddress` | 0x018c15DA1239B84b08283799B89045CD476BBbBb | Receiver's wallet address | +| `projectId` | `replug` | [Your project's ID](https://platform.swing.xyz/) | + +Navigating to our `src/services/requests.ts` file, you will find our method for getting a quote from Swing's Cross-Chain API called `getQuoteRequest()`. + +```typescript +async getQuoteRequest( + queryParams: QuoteQueryParams, +): Promise { + try { + const response = await this.swingSDK.crossChainAPI.GET( + "/v0/transfer/quote", + { + params: { + query: queryParams, + }, + }, + ); + return response.data; + } catch (error) { + console.error("Error fetching quote:", error); + throw error; + } +} +``` + +The response received from the `getQuoteRequest` endpoint provides us with the `fees` a user will have to pay when performing a transaction, as well as a list of possible `routes` for the user to choose from. + +The definition for the `getQuoteRequest` response can be found in `src/interfaces/quote.interface.ts.` + +```typescript +export interface QuoteAPIResponse { + routes: Route[]; + fromToken: Token; + fromChain: Chain; + toToken: Token; + toChain: Chain; +} +``` + +Each `Route` contains a `gasFee`, `bridgeFee` and the amount of tokens the destination wallet will receive. + +Here's an example response that contains the route data: + +```json +"routes": [ + { + "duration": 1, + "gas": "5780346820809249", + "quote": { + "integration": "mayan-finance", + "type": "swap", + "mode": "regular", + "bridgeFee": "3014309889366041", + "bridgeFeeInNativeToken": "0", + "amount": "1549736845026079", + "decimals": 18, + "amountUSD": "7.052", + "bridgeFeeUSD": "13.716", + "bridgeFeeInNativeTokenUSD": "0", + "executionTime": 1138, + "priceImpact": "-66.033", + "fees": [ + { + "type": "bridge", + "amount": "3014309889366041", + "amountUSD": "13.716", + "chainSlug": "ethereum", + "tokenSymbol": "ETH", + "tokenAddress": "0x0000000000000000000000000000000000000000", + "decimals": 18, + "deductedFromSourceToken": true + }, + { + "type": "gas", + "amount": "5780346820809249", + "amountUSD": "0.02000", + "chainSlug": "sui", + "tokenSymbol": "sui", + "tokenAddress": "0x0000000000000000000000000000000000000000000000000000000000000002", + "decimals": 18, + "deductedFromSourceToken": false + }, + { + "type": "partner", + "amount": "0", + "amountUSD": "0", + "chainSlug": "ethereum", + "tokenSymbol": "USDC", + "tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "decimals": 6, + "deductedFromSourceToken": false + } + ] + }, + "route": [ + { + "bridge": "mayan-finance", + "bridgeTokenAddress": "0x2::sui::SUI", + "steps": [ + "allowance", + "approve", + "send" + ], + "path": [ + { + "fromToken": { + "address": "0x2::sui::SUI", + "symbol": "SUI", + "decimals": 9, + "chainSlug": "sui", + "logoURI": "https://s3.ap-northeast-1.amazonaws.com/platform.swing.xyz/assets/sui/0beaf5f2701b3d57daadb0f83f8dae77c23658aeca1d5958402657122b0fa02f.png", + "name": "SUI" + }, + "toToken": { + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "decimals": 18, + "chainSlug": "ethereum", + "logoURI": "https://raw.githubusercontent.com/polkaswitch/assets/master/blockchains/solana/assets/7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs/eth.jpg", + "name": "ETH" + }, + "fromChain": "sui", + "toChain": "ethereum", + "distribution": { + "mayan-finance": 1 + }, + "integration": "mayan-finance", + "amount": "6000000000" + } + ] + } + ], + "distribution": { + "mayan-finance": 1 + }, + "gasUSD": "0.02000" + } +] +``` + +Navigating to our `src/components/Swap.tsx` file, you'll find our `defaultTransferParams` object which will store the default transaction config for our example: + +```typescript +const defaultTransferParams: TransferParams = { + tokenAmount: "1", + fromChain: "ethereum", + tokenSymbol: "ETH", + fromUserAddress: "", + fromTokenAddress: "0x0000000000000000000000000000000000000000", + fromNativeTokenSymbol: "ETH", + fromTokenIconUrl: + "https://raw.githubusercontent.com/Pymmdrza/Cryptocurrency_Logos/mainx/PNG/eth.png", + fromChainIconUrl: + "https://raw.githubusercontent.com/polkaswitch/assets/master/blockchains/ethereum/info/logo.png", + fromChainDecimal: 18, + toTokenAddress: "0x2::sui::SUI", + toTokenSymbol: "SUI", + toNativeTokenSymbol: "SUI", + toChain: "sui", + toTokenIconUrl: + "https://s3.ap-northeast-1.amazonaws.com/platform.swing.xyz/assets/sui/0beaf5f2701b3d57daadb0f83f8dae77c23658aeca1d5958402657122b0fa02f.png", + toChainIconUrl: + "https://s3.ap-northeast-1.amazonaws.com/platform.swing.xyz/assets/sui/0beaf5f2701b3d57daadb0f83f8dae77c23658aeca1d5958402657122b0fa02f.png", + toUserAddress: + "0x308e0f34cd38d6e28912367abe9d044664396aa2569b274198bbb09a8302c030", // sui wallet address + toChainDecimal: 9, +}; +``` + +## Sending a Token Approval Request for ERC20 Tokens (EVM only) + +If you're attempting to bridge an ERC20 token from a user's wallet to Sui, you need to prompt the user to approve the required amount of tokens to be bridged. + +Navigating to our `src/components/Swap.tsx` file, inside our `startTransfer()` method, you will find our implementation of the `getAllowanceRequest()` and `getApprovalTxDataRequest()` methods. Before approving, you have to perform two checks: + +- First, we will check if we're performing a native currency swap by comparing the values of `tokenSymbol` and `fromNativeTokenSymbol` on the source chain. If we're not dealing with a native currency swap, we then proceed to ask for an allowance. +- Next, we will check if an allowance has already been made by Swing on a user's wallet by calling the `getAllowanceRequest()` method. If no approved allowance is found, we will then proceed to make an approval request by calling the `getApprovalTxDataRequest()` method. + +Since the `/approval` and `/approve` endpoints are specific to EVM chains, we have to check that the source chain via `fromChain` is anything but `sui`. Skipping this check will result in the `/approval` endpoint returning an error to the user: + +```json +{ + "statusCode": 400, + "message": "Non-evm is not supported for approval method: sui", + "error": "Bad Request" +} +``` + +Let's execute these steps: + +```typescript +if ( + transferParams.tokenSymbol !== transferParams.fromNativeTokenSymbol && + transferParams.fromChain !== "sui" +) { + const checkAllowance = await getAllowanceRequest({ + bridge: transferRoute.quote.integration, + fromAddress: transferParams.fromUserAddress, + fromChain: transferParams.fromChain, + tokenAddress: transferParams.fromTokenAddress, + tokenSymbol: transferParams.tokenSymbol, + toChain: transferParams.toChain, + toTokenAddress: transferParams.toTokenAddress!, + toTokenSymbol: transferParams.toTokenSymbol!, + contractCall: false, + }); + + if (checkAllowance.allowance === tokenAmount) { + setTransStatus({ + status: `Wallet Interaction Required: Approval Token`, + }); + + const getApprovalTxData = await getApprovalTxDataRequest({ + tokenAmount: Number(tokenAmount), + bridge: transferRoute.quote.integration, + fromAddress: transferParams.fromUserAddress, + fromChain: transferParams.fromChain, + tokenAddress: transferParams.fromTokenAddress, + tokenSymbol: transferParams.tokenSymbol, + toChain: transferParams.toChain, + toTokenAddress: transferParams.toTokenAddress!, + toTokenSymbol: transferParams.toTokenSymbol!, + contractCall: false, + }); + + const txData: TransactionDetails = { + data: getApprovalTxData.tx[0].data, + from: getApprovalTxData.tx[0].from, + to: getApprovalTxData.tx[0].to, + }; + + const txResponse = await signer?.sendTransaction(txData); + + const receipt = await txResponse?.wait(); + console.log("Transaction receipt:", receipt); + + setTransStatus({ status: "Token allowance approved" }); + } +} +``` + +## Sending a Transaction + +After getting a quote, you'll next have to send a transaction to Swing's Cross-Chain API. + +The steps for sending a transaction are as followed: + +- First, we will make a request to [`https://swap.prod.swing.xyz/v0/transfer/send`](https://developers.swing.xyz/reference/api/cross-chain/d83d0d65028dc-send-transfer) +- Using the `txData` returned from the `/send` request, sign the transaction by using a user's wallet + +### Making a `/send` Request + +Navigating to our `src/services/requests.ts`, you'll find our request implemenation for the `/send` endpoint: + +```typescript +async sendTransactionRequest( + payload: SendTransactionPayload, +): Promise { + try { + const response = await this.swingSDK.crossChainAPI.POST( + "/v0/transfer/send", + { + body: payload, + }, + ); + return response.data; + } catch (error) { + console.error("Error sending transaction:", error); + throw error; + } +} +``` + +The `SendTransactionPayload` body payload contains the `source chain`, `destination chain`, `tokenAmount`, and the desired `route`. + +URL: [https://swap.prod.swing.xyz/v0/transfer/send](https://swap.prod.swing.xyz/v0/transfer/send) + +**Parameters**: + +| Key | Example | Description | +| ------------------ | ------------------------------------------------------------------ | ------------------------------------------------------- | +| `fromChain` | ethereum | The blockchain where the transaction originates. | +| `fromTokenAddress` | 0x0000000000000000000000000000000000000000 | Source Token Address | +| `fromUserAddress` | 0x018c15DA1239B84b08283799B89045CD476BBbBb | Sender's wallet address | +| `tokenSymbol` | ETH | Source Token slug | +| `toTokenAddress` | 0x2::sui::SUI | Destination Token Address. | +| `toChain` | sui | Destination Source slug | +| `toTokenAmount` | 4000000 | Amount of the destination token being received. | +| `toTokenSymbol` | SUI | Destination Chain slug | +| `toUserAddress` | 0x308e0f34cd38d6e28912367abe9d044664396aa2569b274198bbb09a8302c030 | Receiver's wallet address | +| `tokenAmount` | 1000000000000000000 | Amount of the source token being sent (in wei for ETH). | +| `type` | swap | Type of transaction. | +| `projectId` | `replug` | [Your project's ID](https://platform.swing.xyz/) | +| `route` | see `Route` in`src/interfaces/send.interface.ts` | Selected Route | + +Since performing a swap will change the state of a user's wallet, the next step of this transaction must be done via a wallet-signed transaction and not via Swing's Cross-Chain API. The response received from the `sendTransactionRequest` endpoint provides us with the necessary `txData/callData` needed to be passed on to a user's wallet to sign the transaction. + +The `txData` from the `sendTransactionRequest` will look something like this: + +```json +{ + .... + "tx": { + "from": "0x018c15DA1239B84b08283799B89045CD476BBbBb", + "to": "0x39E3e49C99834C9573c9FC7Ff5A4B226cD7B0E63", + "data": "0x301a3720000000000000000000000000eeeeeeeeeeee........", + "value": "0x0e35fa931a0000", + "gas": "0x06a02f" + } + .... +} + +``` + +To demonstrate, we first make a request by calling the `sendTransactionRequest` method. + +```typescript +// src/components/Swaps.tsx + +const transfer = await sendTransactionRequest({ + fromChain: transferParams.fromChain, + fromTokenAddress: transferParams.fromTokenAddress, + fromUserAddress: transferParams.fromUserAddress, + tokenSymbol: transferParams.tokenSymbol, + + toTokenAddress: transferParams.toTokenAddress!, + toChain: transferParams.toChain, + toTokenAmount: transferRoute.quote.amount, + toTokenSymbol: transferParams.toTokenSymbol!, + toUserAddress: transferParams.toUserAddress!, + + tokenAmount: convertEthToWei( + transferParams.tokenAmount, + transferParams.fromChainDecimal, + ), + route: transferRoute.route, + type: "swap", +}); +``` + +Next, we'll extract the `txData`: + +```typescript +// src/components/Swaps.tsx + +let txData: any = { + data: transfer.tx.data, + from: transfer.tx.from, + to: transfer.tx.to, + value: transfer.tx.value, + gasLimit: transfer.tx.gas, +}; +``` + +Using our wallet provider, we will send a transaction to the user's wallet. + +```typescript +// src/components/Swaps.tsx + +const txResponse = await signer?.sendTransaction(txData); // <- `txResponse` contains the `txHash` of our transaction. You will need this later for getting a transaction's status. + +const receipt = await txResponse?.wait(); + +console.log("Transaction receipt:", receipt); +``` + +The definition for the `sendTransactionRequest` response can be found in `src/interfaces/send.interface.ts.` + +```typescript +export interface SendTransactionApiResponse { + id: number; + fromToken: Token; + toToken: Token; + fromChain: Chain; + toChain: Chain; + route: Route[]; + tx: TransactionDetails; +} +``` + +> The `sendTransactionRequest` will return and `id` whilst the `txResponse` will contain a `txHash` which we will need later for checking the status of a transaction. + +### Sending a Sui Transaction to the Network + +If you perform a cross-chain swap with Sui as the source chain, you'll sign the transaction using a Sui-compatible wallet (e.g., via `@mysten/dapp-kit`). + +> Remember, you'll have to call the `/send` endpoint via `sendTransactionRequest` before signing the transaction. + +In `src/components/Swap.tsx`, we normalize the `txData.data` from the `/send` response which may be hex-encoded or base64-encoded bytes, construct a `TransactionBlock`, and then sign and execute: + +```typescript +import { TransactionBlock } from "@mysten/sui.js/transactions"; + +function hexToBytes(hex: string): Uint8Array { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex; + const bytes = new Uint8Array(clean.length / 2); + for (let i = 0; i < clean.length; i += 2) + bytes[i / 2] = parseInt(clean.substr(i, 2), 16); + return bytes; +} + +function base64ToBytes(b64: string): Uint8Array { + const bin = atob(b64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return bytes; +} + +const raw = txData.data as string; // from Swing /send response +const isHex = /^[0-9a-fA-F]+$/.test(raw.replace(/^0x/, "")); +const bytes = isHex ? hexToBytes(raw) : base64ToBytes(raw); +const txb = TransactionBlock.from(bytes); + +const result = await wallet.signAndExecuteTransactionBlock({ + transactionBlock: txb, +}); +const txHash = result.digest; +``` + +The UI uses `ConnectButton` from `@mysten/dapp-kit` to connect a Sui wallet. + +## Polling Transaction Status + +After sending a transaction over to the network, for the sake of user experience, it's best to poll the transaction status endpoint by periodically checking to see if the transaction is complete. This will let the user using your dapp know in realtime, the status of the current transaction. + +Navigating to our `src/services/requests.ts` file, you will find our method for getting a transaction status called `getTransationStatus()`. + +```typescript +async getTransationStatusRequest( + queryParams: TransactionStatusParams, +): Promise { + try { + const response = await this.swingSDK.platformAPI.GET( + "/projects/{projectId}/transactions/{transactionId}", + { + params: { + path: { + transactionId: queryParams.id, + projectId, + }, + query: { + txHash: queryParams.txHash, + }, + }, + }, + ); + + return response.data; + } catch (error) { + console.error("Error fetching transaction status:", error); + throw error; + } +} +``` + +The `TransactionStatusParams` params contains the three properties, namely: `id`, `txHash` and `projectId` + +URL: [https://platform.swing.xyz/api/v1/projects/{projectId}/transactions/{transactionId}](https://developers.swing.xyz/reference/api/platform/ehwqfo1kv00ce-get-transaction) + +**Parameters**: + +| Key | Example | Description | +| ----------- | ----------------------------------- | ------------------------------------------------ | +| `id` | 239750 | Transaction ID from `/send` response | +| `txHash` | 0x3b2a04e2d16489bcbbb10960a248..... | The transaction hash identifier. | +| `projectId` | `replug` | [Your project's ID](https://platform.swing.xyz/) | + +To poll the `/status` endpoint, we'll be using `setTimeout()` to to retry `getTransationStatus()` over a period of time. We will define a function, `pollTransactionStatus()`, which will recursively call `getTransStatus()` until the transaction is completed. + +```typescript +// src/components/Swaps.tsx + +async function getTransStatus(transId: string, txHash: string) { + const transactionStatus = await swingServiceAPI?.getTransationStatusRequest({ + id: transId, + txHash, + }); + + setTransStatus(transactionStatus); + + return transactionStatus; +} + +async function pollTransactionStatus(transId: string, txHash: string) { + const transactionStatus = await getTransStatus(transId, txHash); + + if (transactionStatus?.status! === "Pending") { + setTimeout( + () => pollTransactionStatus(transId, txHash), + transactionPollingDuration, + ); + } else { + if (transactionStatus?.status === "Success") { + toast({ + title: "Transaction Successful", + description: `Bridge Successful`, + }); + } else if (transactionStatus?.status === "Failed") { + toast({ + variant: "destructive", + title: "Transaction Failed", + description: transStatus?.errorReason, + }); + } + + setTransferRoute(null); + setIsTransacting(false); + (sendInputRef.current as HTMLInputElement).value = ""; + } +} +``` + +In our `startTransfer()` method, we will execute the `pollTransactionStatus()` right after our transaction is sent over the network + +```typescript +// src/components/Swap.tsx + +let txHash = ""; + +if (transferParams.fromChain === "sui") { + const hash = await sendSuiTrans({ + ...txData, + from: transferParams.fromUserAddress, + }); + txHash = hash!; // Sui digest +} else { + const txResponse = await signer?.sendTransaction({ + data: txData.data, + from: txData.from, + to: txData.to, + value: txData.value, + gasLimit: txData.gasLimit, + }); + const receipt = await txResponse?.wait(); + console.log("Transaction receipt:", receipt); + txHash = txResponse?.hash!; +} + +pollTransactionStatus(transfer?.id.toString()!, txHash); +``` + +## Customizing + +You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files. diff --git a/examples/swaps-api-nextjs-sui/components.json b/examples/swaps-api-nextjs-sui/components.json new file mode 100644 index 0000000..44d0040 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "zinc", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "components", + "utils": "lib" + } +} \ No newline at end of file diff --git a/examples/swaps-api-nextjs-sui/next-env.d.ts b/examples/swaps-api-nextjs-sui/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/swaps-api-nextjs-sui/next.config.mjs b/examples/swaps-api-nextjs-sui/next.config.mjs new file mode 100644 index 0000000..d5456a1 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/examples/swaps-api-nextjs-sui/package.json b/examples/swaps-api-nextjs-sui/package.json new file mode 100644 index 0000000..edeb7bf --- /dev/null +++ b/examples/swaps-api-nextjs-sui/package.json @@ -0,0 +1,60 @@ +{ + "name": "swaps-api-nextjs-sui", + "demo": "https://swaps-api-nextjs-sui.vercel.app/", + "keywords": [ + "api", + "swaps", + "nextjs", + "thirdweb" + ], + "license": "MIT", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "eslint src", + "build": "next build", + "start": "next start", + "clean": "rm -rf .next node_modules" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-brands-svg-icons": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", + "@headlessui/react": "^1.7.18", + "@mysten/sui.js": "^0.54.1", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-toast": "^1.1.5", + "@suiet/wallet-kit": "^0.2.25", + "@swing.xyz/sdk": "^0.60.0", + "@tailwindcss/forms": "^0.5.7", + "@thirdweb-dev/react": "^4.4.17", + "@thirdweb-dev/sdk": "^4.0.44", + "axios": "^1.6.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "framer-motion": "^6.5.1", + "lucide-react": "^0.368.0", + "next": "14.1.3", + "react": "18.2.0", + "react-dom": "18.2.0", + "tailwind-merge": "^2.2.2", + "tailwindcss-animate": "^1.0.7", + "use-debounce": "^10.0.0" + }, + "devDependencies": { + "@types/node": "20.11.26", + "@types/react": "18.2.65", + "@types/react-dom": "18.2.22", + "autoprefixer": "^10.4.18", + "eslint": "^8.57.0", + "eslint-config-examples": "workspace:*", + "pino-pretty": "^10.3.1", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.4.2", + "typescript-config": "workspace:*" + } +} diff --git a/examples/swaps-api-nextjs-sui/postcss.config.js b/examples/swaps-api-nextjs-sui/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/examples/swaps-api-nextjs-sui/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/swaps-api-nextjs-sui/public/favicon.ico b/examples/swaps-api-nextjs-sui/public/favicon.ico new file mode 100644 index 0000000..88b1f7a Binary files /dev/null and b/examples/swaps-api-nextjs-sui/public/favicon.ico differ diff --git a/examples/swaps-api-nextjs-sui/public/fonts/Inter-italic.var.woff2 b/examples/swaps-api-nextjs-sui/public/fonts/Inter-italic.var.woff2 new file mode 100644 index 0000000..b826d5a Binary files /dev/null and b/examples/swaps-api-nextjs-sui/public/fonts/Inter-italic.var.woff2 differ diff --git a/examples/swaps-api-nextjs-sui/public/fonts/Inter-roman.var.woff2 b/examples/swaps-api-nextjs-sui/public/fonts/Inter-roman.var.woff2 new file mode 100644 index 0000000..6a256a0 Binary files /dev/null and b/examples/swaps-api-nextjs-sui/public/fonts/Inter-roman.var.woff2 differ diff --git a/examples/swaps-api-nextjs-sui/src/app/layout.tsx b/examples/swaps-api-nextjs-sui/src/app/layout.tsx new file mode 100644 index 0000000..f3f5117 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/app/layout.tsx @@ -0,0 +1,27 @@ +import "styles/globals.css"; +import "@fortawesome/fontawesome-svg-core/styles.css"; + +import { Header } from "../components/ui/Header"; +import dynamic from "next/dynamic"; + +import { Toaster } from "components/ui/toaster"; + +export default function Layout({ children }: { children: React.ReactNode }) { + const SuiWalletProviders = dynamic( + () => import("components/providers/SuiWalletProviders"), + { ssr: false }, + ); + return ( + + + + + +
+
{children}
+ + + + + ); +} diff --git a/examples/swaps-api-nextjs-sui/src/app/page.tsx b/examples/swaps-api-nextjs-sui/src/app/page.tsx new file mode 100644 index 0000000..d066105 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/app/page.tsx @@ -0,0 +1,12 @@ +import { Hero } from "../components/ui/Hero"; +import { ThirdwebProvider } from "../components/ThirdwebProvider"; +import { Backdrop } from "components/ui/Backdrop"; + +export default function Home() { + return ( + + + + + ); +} diff --git a/examples/swaps-api-nextjs-sui/src/components/Swap.tsx b/examples/swaps-api-nextjs-sui/src/components/Swap.tsx new file mode 100644 index 0000000..dbe07fd --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/Swap.tsx @@ -0,0 +1,935 @@ +"use client"; + +import clsx from "clsx"; +import { useEffect, useRef, useState } from "react"; +import { useConnect, metamaskWallet } from "@thirdweb-dev/react"; +import { + useConnectionStatus, + useAddress, + useSigner, +} from "@thirdweb-dev/react"; +import { SwingServiceAPI } from "services/requests"; +import { convertEthToWei, convertWeiToEth } from "utils/ethToWei"; +import { useDebouncedCallback } from "use-debounce"; +import { useToast } from "components/ui/use-toast"; +import { TransactionStatusAPIResponse } from "interfaces/status.interface"; +import { Chain } from "interfaces/chain.interface"; +import { Token } from "interfaces/token.interface"; +import { SelectTokenPanel } from "./ui/SelectTokenPanel"; +import { SelectChainPanel } from "./ui/SelectChainPanel"; +import { TbSwitchVertical, TbSwitchHorizontal } from "react-icons/tb"; +import { faCircleNotch } from "@fortawesome/free-solid-svg-icons"; + +import { useWallet, ConnectButton } from "@suiet/wallet-kit"; +import { TransactionBlock } from "@mysten/sui.js/transactions"; +import { TransferParams } from "types/transfer.types"; +import { TransferHistoryPanel } from "./ui/TransferHistoryPanel"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ISwingServiceAPI } from "interfaces/swing-service.interface"; +import { Route } from "interfaces/quote.interface"; +import { TransactionData } from "interfaces/approval.interface"; + +const walletConfig = metamaskWallet(); + +const defaultTransferParams: TransferParams = { + tokenAmount: "1", + fromChain: "ethereum", + tokenSymbol: "ETH", + fromUserAddress: "", + fromTokenAddress: "0x0000000000000000000000000000000000000000", + fromNativeTokenSymbol: "ETH", + fromTokenIconUrl: + "https://raw.githubusercontent.com/Pymmdrza/Cryptocurrency_Logos/mainx/PNG/eth.png", + fromChainIconUrl: + "https://raw.githubusercontent.com/polkaswitch/assets/master/blockchains/ethereum/info/logo.png", + fromChainDecimal: 18, + toTokenAddress: "0x2::sui::SUI", + toTokenSymbol: "SUI", + toNativeTokenSymbol: "SUI", + toChain: "sui", + toTokenIconUrl: + "https://s3.ap-northeast-1.amazonaws.com/platform.swing.xyz/assets/sui/0beaf5f2701b3d57daadb0f83f8dae77c23658aeca1d5958402657122b0fa02f.png", + toChainIconUrl: + "https://s3.ap-northeast-1.amazonaws.com/platform.swing.xyz/assets/sui/0beaf5f2701b3d57daadb0f83f8dae77c23658aeca1d5958402657122b0fa02f.png", + toUserAddress: + "0x308e0f34cd38d6e28912367abe9d044664396aa2569b274198bbb09a8302c030", // sui wallet address + toChainDecimal: 9, +}; + +const transactionPollingDuration = 10000; + +const Swap = () => { + const [isLoading, setIsLoading] = useState(false); + const [isTransacting, setIsTransacting] = useState(false); + + const [transferParams, setTransferParams] = useState( + defaultTransferParams, + ); + + const [transferRoute, setTransferRoute] = useState(null); + const [transStatus, setTransStatus] = + useState(); + const [tokens, setTokens] = useState([]); + const [suiTokens, setSuiTokens] = useState([]); + const [chains, setChains] = useState([]); + const [swingServiceAPI, setSwingServiceAPI] = useState< + ISwingServiceAPI | undefined + >(); + + const connect = useConnect(); + const address = useAddress(); + const wallet = useWallet(); + + const connectionStatus = useConnectionStatus(); + const walletAddress = useAddress(); + const signer = useSigner(); + + const { toast } = useToast(); + const sendInputRef = useRef(null); + + const debounced = useDebouncedCallback((value) => { + setTransferParams((prev: TransferParams) => ({ + ...prev, + tokenAmount: value, + })); + getQuote(value); + }, 1000); + + //Initialize Swing Service API from service.ts file + useEffect(() => { + setSwingServiceAPI(new SwingServiceAPI()); + }, []); + + //Fetch chains and tokens whenever wallet address changes + useEffect(() => { + setTransferParams((prev) => { + return { + ...prev, + fromUserAddress: walletAddress!, + }; + }); + + setChains([]); + setTokens([]); + setSuiTokens([]); + + swingServiceAPI + ?.getChainsRequest({ type: "evm" }) + .then((chains: Chain[] | undefined) => { + setChains(chains!); + }); + + swingServiceAPI + ?.getTokensRequest({ chain: defaultTransferParams.fromChain }) + .then((tokens: Token[] | undefined) => { + setTokens(tokens!); + }); + + swingServiceAPI + ?.getTokensRequest({ chain: "sui" }) + .then((tokens: Token[] | undefined) => { + setSuiTokens(tokens!); + }); + }, [walletAddress]); + + // Keep SUI addresses in sync with connected SUI wallet + useEffect(() => { + if (!wallet?.address) return; + setTransferParams((prev) => ({ + ...prev, + ...(prev.fromChain === "sui" ? { fromUserAddress: wallet.address! } : {}), + ...(prev.toChain === "sui" ? { toUserAddress: wallet.address! } : {}), + })); + }, [wallet?.address]); + + //Connect to Ethereum Wallet + async function connectWallet(chainId?: number) { + try { + // Connect to MetaMask + await connect(walletConfig, { chainId }); + + // Connect wallet signer to Swing SDK + const walletAddress = address; + + setTransferParams((prev) => { + return { + ...prev, + fromUserAddress: walletAddress!, + }; + }); + } catch (error) { + console.error("Connect Wallet Error:", error); + toast({ + variant: "destructive", + title: "Something went wrong!", + description: (error as Error).message, + }); + } + } + + async function getTransStatus(transId: string, txHash: string) { + const transactionStatus = await swingServiceAPI?.getTransationStatusRequest( + { + id: transId, + txHash, + }, + ); + + setTransStatus(transactionStatus); + + return transactionStatus; + } + + async function pollTransactionStatus(transId: string, txHash: string) { + const transactionStatus = await getTransStatus(transId, txHash); + + if (transactionStatus?.status! === "Pending") { + setTimeout( + () => pollTransactionStatus(transId, txHash), + transactionPollingDuration, + ); + } else { + if (transactionStatus?.status === "Success") { + toast({ + title: "Transaction Successful", + description: `Bridge Successful`, + }); + } else if (transactionStatus?.status === "Failed") { + toast({ + variant: "destructive", + title: "Transaction Failed", + description: transStatus?.errorReason, + }); + } + + setTransferRoute(null); + setTransStatus(null); + setIsTransacting(false); + setIsLoading(false); + (sendInputRef.current as HTMLInputElement).value = ""; + } + } + + async function getQuote(value: string) { + if (Number(value) <= 0) { + return; + } + + setIsLoading(true); + + try { + if (transferParams.toUserAddress === "") { + toast({ + variant: "destructive", + title: "SUI Address Not Set", + description: "Please connect your SUI wallet", + }); + return; + } + + const quotes = await swingServiceAPI?.getQuoteRequest({ + fromChain: transferParams.fromChain, + fromTokenAddress: transferParams.fromTokenAddress, + fromUserAddress: transferParams.fromUserAddress, + toChain: transferParams.toChain, + tokenSymbol: transferParams.tokenSymbol, + toTokenAddress: transferParams.toTokenAddress, + toTokenSymbol: transferParams.toTokenSymbol, + toUserAddress: transferParams.toUserAddress, + tokenAmount: convertEthToWei(value, transferParams.fromChainDecimal), + }); + + if (!quotes?.routes?.length) { + toast({ + variant: "destructive", + title: "No routes found", + description: "No routes available. Try increasing the send amount.", + }); + setIsLoading(false); + return; + } + + setTransferRoute(quotes.routes.at(0)!); + } catch (error) { + console.error("Quote Error:", error); + toast({ + variant: "destructive", + title: "Something went wrong!", + description: (error as Error).message, + }); + } + + setIsLoading(false); + } + + // Convert a hex string to Uint8Array (supports both upper/lower case, no 0x prefix expected) + function hexToBytes(hex: string): Uint8Array { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex; + if (clean.length % 2 !== 0) throw new Error("Invalid hex length"); + const bytes = new Uint8Array(clean.length / 2); + for (let i = 0; i < clean.length; i += 2) { + bytes[i / 2] = parseInt(clean.substr(i, 2), 16); + } + return bytes; + } + + // Convert a base64 string to Uint8Array + function base64ToBytes(b64: string): Uint8Array { + if (typeof atob === "function") { + const bin = atob(b64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return bytes; + } + // Fallback for environments without atob (shouldn't happen in client) + return Uint8Array.from(Buffer.from(b64, "base64")); + } + + async function sendSuiTrans( + txData: TransactionData, + ): Promise { + const raw = txData.data!; + + // The backend may return base64 or hex. Normalize to bytes and build a TransactionBlock. + const isHex = /^[0-9a-fA-F]+$/.test(raw.replace(/^0x/, "")); + const bytes = isHex ? hexToBytes(raw) : base64ToBytes(raw); + const txb = TransactionBlock.from(bytes); + + const signedTx = await wallet.signAndExecuteTransactionBlock({ + transactionBlock: txb, + }); + + return signedTx.digest; + } + + function switchTransferParams() { + const tempTransferParams: TransferParams = Object.create(transferParams); + + const newTransferParams: TransferParams = { + tokenAmount: "0", + fromChain: tempTransferParams.toChain, + tokenSymbol: tempTransferParams.toTokenSymbol!, + fromUserAddress: tempTransferParams.toUserAddress!, + fromTokenAddress: tempTransferParams.toTokenAddress!, + fromTokenIconUrl: tempTransferParams.toTokenIconUrl, + fromChainIconUrl: tempTransferParams.toChainIconUrl, + fromChainDecimal: tempTransferParams.toChainDecimal, + fromNativeTokenSymbol: tempTransferParams.toNativeTokenSymbol, + toTokenAddress: tempTransferParams.fromTokenAddress, + toTokenSymbol: tempTransferParams.tokenSymbol, + toChain: tempTransferParams.fromChain, + toChainIconUrl: tempTransferParams.fromChainIconUrl, + toTokenIconUrl: tempTransferParams.fromTokenIconUrl!, + toUserAddress: tempTransferParams.fromUserAddress, + toChainDecimal: tempTransferParams.fromChainDecimal, + toNativeTokenSymbol: tempTransferParams.fromNativeTokenSymbol, + }; + + setTransferRoute(null); + setTransferParams(newTransferParams); + + (sendInputRef.current as HTMLInputElement).value = ""; + } + + function onEVMChainSelect(chain: Chain) { + swingServiceAPI + ?.getTokensRequest({ chain: chain.slug }) + .then((tokens: Token[] | undefined) => { + setTokens(tokens!); + }); + + if (transferParams.fromChain !== "sui") { + setTransferParams((prev) => ({ + ...prev, + tokenAmount: "0", + fromChain: chain.slug, + fromChainIconUrl: chain.logo, + + tokenSymbol: tokens[0].symbol, + fromTokenAddress: tokens[0].address, + fromTokenIconUrl: tokens[0].logo, + fromChainDecimal: tokens[0].decimals, + fromNativeTokenSymbol: chain.nativeToken?.symbol, + })); + } else { + setTransferParams((prev) => ({ + ...prev, + tokenAmount: "0", + toChain: chain.slug, + toChainIconUrl: chain.logo, + + toTokenSymbol: tokens[0].symbol, + toTokenAddress: tokens[0].address, + toTokenIconUrl: tokens[0].logo, + toChainDecimal: tokens[0].decimals, + toNativeTokenSymbol: chain.nativeToken?.symbol, + })); + } + + setTransferRoute(null); + } + + function onFromTokenSelect(token: Token) { + setTransferParams((prev) => ({ + ...prev, + tokenAmount: "0", + tokenSymbol: token.symbol, + fromTokenAddress: token.address, + fromTokenIconUrl: token.logo, + fromChainDecimal: token.decimals, + })); + setTransferRoute(null); + } + + function onToTokenSelect(token: Token) { + setTransferParams((prev) => ({ + ...prev, + tokenAmount: "0", + toTokenSymbol: token.symbol, + toTokenAddress: token.address, + toTokenIconUrl: token.logo, + toChainDecimal: token.decimals, + })); + setTransferRoute(null); + } + + async function startTransfer() { + if (!transferRoute) { + toast({ + variant: "destructive", + title: "Something went wrong!", + description: "Please get a route first before attempting a transaction", + }); + return; + } + + setIsLoading(true); + setIsTransacting(true); + + const tokenAmount = convertEthToWei( + transferParams.tokenAmount, + transferParams.fromChainDecimal, + ); + + try { + if ( + transferParams.tokenSymbol !== transferParams.fromNativeTokenSymbol && + transferParams.fromChain !== "sui" + ) { + const checkAllowance = await swingServiceAPI?.getAllowanceRequest({ + bridge: transferRoute.quote.integration, + fromAddress: transferParams.fromUserAddress, + fromChain: transferParams.fromChain, + tokenAddress: transferParams.fromTokenAddress, + tokenSymbol: transferParams.tokenSymbol, + toChain: transferParams.toChain, + toTokenAddress: transferParams.toTokenAddress!, + toTokenSymbol: transferParams.toTokenSymbol!, + contractCall: false, + }); + + if (checkAllowance?.allowance! != tokenAmount) { + setTransStatus({ + status: `Wallet Interaction Required: Approval Token`, + }); + + const getApprovalTxData = + await swingServiceAPI?.getApprovalTxDataRequest({ + tokenAmount: tokenAmount, + bridge: transferRoute.quote.integration, + fromAddress: transferParams.fromUserAddress, + fromChain: transferParams.fromChain, + tokenAddress: transferParams.fromTokenAddress, + tokenSymbol: transferParams.tokenSymbol, + toChain: transferParams.toChain, + toTokenAddress: transferParams.toTokenAddress!, + toTokenSymbol: transferParams.toTokenSymbol!, + contractCall: false, + }); + + const txData: TransactionData = { + data: getApprovalTxData?.tx?.at(0)?.data!, + from: getApprovalTxData?.tx?.at(0)?.from!, + to: getApprovalTxData?.tx?.at(0)?.to!, + }; + + const txResponse = await signer?.sendTransaction(txData); + + const receipt = await txResponse?.wait(); + console.log("Transaction receipt:", receipt); + + setTransStatus({ status: "Token allowance approved" }); + } + } + + console.log({ + fromChain: transferParams.fromChain, + fromTokenAddress: transferParams.fromTokenAddress, + fromUserAddress: transferParams.fromUserAddress, + tokenSymbol: transferParams.tokenSymbol, + + toTokenAddress: transferParams.toTokenAddress!, + toChain: transferParams.toChain, + toTokenAmount: transferRoute.quote.amount, + toTokenSymbol: transferParams.toTokenSymbol!, + toUserAddress: transferParams.toUserAddress!, + integration: transferRoute.quote.integration, + + tokenAmount, + route: transferRoute.route, + type: "swap", + }); + + const transfer = await swingServiceAPI?.sendTransactionRequest({ + fromChain: transferParams.fromChain, + fromTokenAddress: transferParams.fromTokenAddress, + fromUserAddress: transferParams.fromUserAddress, + tokenSymbol: transferParams.tokenSymbol, + + toTokenAddress: transferParams.toTokenAddress!, + toChain: transferParams.toChain, + toTokenAmount: transferRoute.quote.amount, + toTokenSymbol: transferParams.toTokenSymbol!, + toUserAddress: transferParams.toUserAddress!, + integration: transferRoute.quote.integration, + + tokenAmount, + route: transferRoute.route, + type: "swap", + }); + + if (!transfer) { + toast({ + variant: "destructive", + title: "Something went wrong!", + description: "Transaction Failed", + }); + setIsLoading(false); + setIsTransacting(false); + setTransStatus(null); + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const txData: any = { + data: transfer?.tx?.data, + from: transfer?.tx?.from!, + to: transfer?.tx?.to!, + value: transfer?.tx?.value!, + txId: transfer?.tx?.txId!, + gasLimit: transfer?.tx?.gas!, + }; + + setTransStatus({ + status: "Wallet Interaction Required: Approve Transaction", + }); + + let txHash = ""; + + if (transferParams.fromChain === "sui") { + const hash = await sendSuiTrans({ + ...txData, + from: transferParams.fromUserAddress, + }); + txHash = hash!; + } else { + const txResponse = await signer?.sendTransaction({ + data: txData.data, + from: txData.from, + to: txData.to, + value: txData.value, + gasLimit: txData.gasLimit, + }); + // Wait for the transaction to be mined + + const receipt = await txResponse?.wait(); + console.log("Transaction receipt:", receipt); + txHash = txResponse?.hash!; + } + + pollTransactionStatus(transfer?.id.toString()!, txHash); + } catch (error) { + console.error("Transfer Error:", error); + toast({ + variant: "destructive", + title: "Something went wrong!", + description: "Something went wrong", + }); + + setIsTransacting(false); + setTransStatus(null); + } + } + + function SelectFromChainPanel() { + return ( +
+ {transferParams.fromChain === "sui" ? ( + + ) : ( + + )} +
+ ); + } + + function SelectToChainPanel() { + return ( +
+ {transferParams.toChain === "sui" ? ( + + ) : ( + + )} +
+ ); + } + + function SelectFromTokenPanel() { + return ( +
+ +
+ ); + } + + function SelectToTokenPanel() { + return ( +
+ +
+ ); + } + + return ( +
+
+

BRIDGE

+
+
+ {!isTransacting ? ( +
+ + +
+ +
+ + + + +
+ ) : ( +
+ {transStatus?.status} + +
+ )} +
+ +
+
+ +
+
+

SEND

+
+ {transferParams.fromChain === "sui" && ( + <> + + + )} +
+
+
+ { + debounced(e.target.value); + setTransferRoute(null); // Reset transfer route + }} + type="number" + /> +
+

+ {transferParams.tokenSymbol} +

+ +
+
+ +
+

+
+
+ +
+
+
+ +
+
+
+ +
+
+

RECEIVE

+
+ {transferParams.toChain === "sui" && ( + + )} +
+
+
+ { + setTransferRoute(null); // Reset transfer route + setTransferParams((prev) => ({ + ...prev, + amount: e.target.value, + })); + }} + /> +
+

+ {transferParams.toTokenSymbol} +

+ +
+
+ +
+

+ {formatUSD(transferRoute?.quote?.amountUSD! ?? 0)} +

+
+
+ +
+
+

+ BEST ROUTE +

+ {!transferRoute ? ( +
NO ROUTE FOUND
+ ) : ( +
+ {transferRoute.quote.integration.toUpperCase()} +
+ )} +
+
+

+ GAS FEE +

+
+ {formatUSD(transferRoute?.gasUSD! ?? 0)} +
+
+
+

TOTAL

+
+ {formatUSD(transferRoute?.quote?.amountUSD! ?? 0)} +
+
+
+ +
+ +
+
+ ); +}; + +function formatUSD(amount: string) { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + }).format(Number(amount)); +} + +export default Swap; diff --git a/examples/swaps-api-nextjs-sui/src/components/ThirdwebProvider.tsx b/examples/swaps-api-nextjs-sui/src/components/ThirdwebProvider.tsx new file mode 100644 index 0000000..b420a91 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ThirdwebProvider.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { ThirdwebProvider } from "@thirdweb-dev/react"; diff --git a/examples/swaps-api-nextjs-sui/src/components/providers/SuiWalletProviders.tsx b/examples/swaps-api-nextjs-sui/src/components/providers/SuiWalletProviders.tsx new file mode 100644 index 0000000..c62c388 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/providers/SuiWalletProviders.tsx @@ -0,0 +1,13 @@ +"use client"; + +import React from "react"; +import { WalletProvider } from "@suiet/wallet-kit"; +import "@suiet/wallet-kit/style.css"; + +export default function SuiWalletProviders({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/Backdrop.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/Backdrop.tsx new file mode 100644 index 0000000..ba7cc87 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/Backdrop.tsx @@ -0,0 +1,7 @@ +export const Backdrop = () => { + return ( +
+
+
+ ); +}; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/Button.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/Button.tsx new file mode 100644 index 0000000..251668b --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/Button.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import Link from "next/link"; +import clsx from "clsx"; + +const baseStyles = { + solid: + "inline-flex justify-center rounded-2xl py-2 px-3 text-sm font-semibold outline-2 outline-offset-2 transition-colors", + outline: + "inline-flex justify-center rounded-2xl border py-[calc(theme(spacing.2)-1px)] px-[calc(theme(spacing.3)-1px)] text-sm outline-2 outline-offset-2 transition-colors", +}; + +const variantStyles = { + solid: { + cyan: "relative overflow-hidden bg-cyan-500 text-white before:absolute before:inset-0 active:before:bg-transparent hover:before:bg-white/10 active:bg-cyan-600 active:text-white/80 before:transition-colors", + white: + "bg-white text-cyan-900 hover:bg-white/90 active:bg-white/90 active:text-cyan-900/70", + gray: "bg-gray-800 text-white hover:bg-gray-900 active:bg-gray-800 active:text-white/80", + }, + outline: { + gray: "border-gray-300 text-gray-700 hover:border-gray-400 active:bg-gray-100 active:text-gray-700/80", + }, +}; + +export const Button = function Button({ + variant = "solid", + color = "gray", + className, + href, + ...props +}: { + variant?: "solid" | "outline"; + color?: "cyan" | "white" | "gray"; + className?: string; + href?: string; +} & React.InputHTMLAttributes) { + const styles = variantStyles[variant]; + + className = clsx( + baseStyles[variant], + color in styles ? styles[color as keyof typeof styles] : {}, + className, + ); + + const isExternalLink = href?.startsWith("http"); + + return href ? ( + + ) : ( + + ); +}; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/ChainTokenItem.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/ChainTokenItem.tsx new file mode 100644 index 0000000..e06ed75 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/ChainTokenItem.tsx @@ -0,0 +1,17 @@ +export const ChainTokenItem = ({ + logo, + name, + onItemSelect, +}: { + logo: string; + name: string; + onItemSelect?: () => void | undefined; +}) => ( +
onItemSelect?.()} + > + {name} + {name} +
+); diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/Container.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/Container.tsx new file mode 100644 index 0000000..af45582 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/Container.tsx @@ -0,0 +1,17 @@ +import clsx from "clsx"; + +export function Container({ + className, + children, +}: { + className?: string; + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/Header.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/Header.tsx new file mode 100644 index 0000000..44ec04e --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/Header.tsx @@ -0,0 +1,161 @@ +"use client"; + +import Link from "next/link"; +import { Popover } from "@headlessui/react"; +import { AnimatePresence, motion } from "framer-motion"; + +import { Button } from "./Button"; +import { Container } from "./Container"; +import { NavLinks } from "./NavLinks"; +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faGithub } from "@fortawesome/free-brands-svg-icons"; + +function MenuIcon({ className }: { className?: string }) { + return ( + + ); +} + +function ChevronUpIcon({ className }: { className?: string }) { + return ( + + ); +} + +function MobileNavLink({ + children, + href, +}: { + children: React.ReactNode; + href: string; +}) { + return ( + + {children} + + ); +} + +export function Header() { + return ( +
+ +
+ ); +} diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/Hero.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/Hero.tsx new file mode 100644 index 0000000..db59e4d --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/Hero.tsx @@ -0,0 +1,43 @@ +"use client"; + +import React from "react"; +import { Container } from "./Container"; + +import SwapSDK from "../Swap"; + +export function Hero() { + return ( +
+ +
+
+ {/* {"sols"} + {"sols2"} */} +
+
+

+ Crazy Fast
+ Bridging On Sui! +

+

+ © Copyright {new Date().getFullYear()}. All rights reserved. +

+
+
+
+ +
+
+
+ ); +} diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/NavLinks.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/NavLinks.tsx new file mode 100644 index 0000000..ff70568 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/NavLinks.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import Link from "next/link"; +import { AnimatePresence, motion } from "framer-motion"; + +export function NavLinks() { + const [hoveredIndex, setHoveredIndex] = useState(null); + + return ( + <> + {[ + ["Swap", "#"], + ["Governance", "#"], + ["Research", "#"], + ["Documentation", "#"], + ].map(([label, href], index) => ( + setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + > + + {hoveredIndex === index && ( + + )} + + {label} + + ))} + + ); +} diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/SelectChainPanel.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/SelectChainPanel.tsx new file mode 100644 index 0000000..88e453c --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/SelectChainPanel.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; +import { ChainTokenItem } from "./ChainTokenItem"; +import { Chain } from "interfaces/chain.interface"; + +const allowedChains = ["ethereum", "polygon", "avalanche"]; + +export const SelectChainPanel = ({ + chains, + transferParams, + onChainSelect, +}: { + chains: Chain[]; + transferParams: { chainIconUrl: string; chain: string | undefined }; + onChainSelect?: (chain: Chain) => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + + {chains.map( + (chain) => + allowedChains.includes(chain.slug) && ( + { + onChainSelect?.(chain); + setIsOpen(false); + }} + /> + ), + )} + + + ); +}; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/SelectTokenPanel.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/SelectTokenPanel.tsx new file mode 100644 index 0000000..b4b57cf --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/SelectTokenPanel.tsx @@ -0,0 +1,63 @@ +import { useEffect, useState } from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; +import { Token } from "interfaces/token.interface"; +import { ChainTokenItem } from "./ChainTokenItem"; + +export const SelectTokenPanel = ({ + tokens, + transferParams, + onTokenSelect, +}: { + tokens: Token[]; + transferParams: { + tokenIconUrl: string; + chain: string | undefined; + token: string | undefined; + }; + onTokenSelect?: (token: Token) => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const [filteredTokens, setFilteredTokens] = useState(tokens); + + useEffect(() => { + if (isOpen) { + setFilteredTokens(tokens); + } + }, [isOpen]); + + return ( + + + {transferParams.token} + + + { + const tokenResults = tokens?.filter((token) => + token.symbol + .toLowerCase() + .startsWith(e.target.value.toLowerCase()), + ); + setFilteredTokens(() => [...tokenResults!]); + }} + /> + {filteredTokens?.map((token, index) => ( + { + onTokenSelect?.(token); + setIsOpen(false); + }} + /> + ))} + + + ); +}; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/TransferHistoryPanel.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/TransferHistoryPanel.tsx new file mode 100644 index 0000000..ddc59a9 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/TransferHistoryPanel.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; +import { + Transaction, + TransactionResponseAPIResponse, +} from "interfaces/history.interface"; +import { MdOutlineHistory } from "react-icons/md"; +import clsx from "clsx"; +import { pendingStatuses } from "interfaces/send.interface"; +import { ISwingServiceAPI } from "interfaces/swing-service.interface"; + +export const TransferHistoryPanel = ({ + swingServiceAPI, + userAddress = "", + className, + onItemSelect, +}: { + userAddress: string; + className?: string; + onItemSelect?: (token: Transaction) => void; + swingServiceAPI: ISwingServiceAPI; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const [historyList, sethistoryList] = useState([]); + const [filteredItems, setFilteredItems] = useState( + [], + ); + + useEffect(() => { + if (isOpen && userAddress.length) { + swingServiceAPI + ?.getTransationHistoryRequest({ userAddress }) + .then((response: TransactionResponseAPIResponse | undefined) => { + sethistoryList(response?.transactions); + setFilteredItems(response?.transactions); + }); + } + }, [isOpen]); + + return ( + + + + + + { + const historyResults = historyList?.filter( + (history) => + history.status + .toLowerCase() + .startsWith(e.target.value.toLowerCase()) || + history + .fromChainSlug!.toLowerCase() + .startsWith(e.target.value.toLowerCase()) || + history + .toChainSlug!.toLowerCase() + .startsWith(e.target.value.toLowerCase()), + ); + setFilteredItems(() => [...historyResults!]); + }} + /> + {filteredItems?.reverse().map((transaction, index) => ( +
onItemSelect?.(transaction)} + > +
+ ROUTE + + {transaction.fromChainSlug?.toUpperCase().substring(0, 3)} ( + {transaction.fromTokenSymbol}) {">"}{" "} + {transaction.toChainSlug?.toUpperCase().substring(0, 3)} ( + {transaction.toTokenSymbol}) + +
+ +
+ AMOUNT + + {transaction.toAmountUsdValue} USD + +
+ +
+ STATUS + + {transaction.status} + +
+
+ ))} +
+
+ ); +}; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/input.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/input.tsx new file mode 100644 index 0000000..433ae98 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "lib"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/label.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/label.tsx new file mode 100644 index 0000000..644132e --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "lib"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/popover.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/popover.tsx new file mode 100644 index 0000000..efbc6cf --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; + +import { cn } from "lib"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/toast.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/toast.tsx new file mode 100644 index 0000000..550ac85 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/toast.tsx @@ -0,0 +1,130 @@ +"use client"; + +import * as React from "react"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; + +import { cn } from "lib"; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl border border-zinc-200 p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-zinc-800", + { + variants: { + variant: { + default: + "border bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50", + destructive: + "destructive group border-red-500 bg-red-500 text-zinc-50 dark:border-red-900 dark:bg-red-900 dark:text-zinc-50", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +}; diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/toaster.tsx b/examples/swaps-api-nextjs-sui/src/components/ui/toaster.tsx new file mode 100644 index 0000000..32376bc --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "components/ui/toast"; +import { useToast } from "components/ui/use-toast"; + +export function Toaster() { + const { toasts } = useToast(); + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/examples/swaps-api-nextjs-sui/src/components/ui/use-toast.ts b/examples/swaps-api-nextjs-sui/src/components/ui/use-toast.ts new file mode 100644 index 0000000..7bb041c --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/components/ui/use-toast.ts @@ -0,0 +1,191 @@ +"use client"; + +// Inspired by react-hot-toast library +import * as React from "react"; + +import type { ToastActionElement, ToastProps } from "components/ui/toast"; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof actionTypes; + +type Action = + | { + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; + } + | { + type: ActionType["UPDATE_TOAST"]; + toast: Partial; + } + | { + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; + } + | { + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t, + ), + }; + + case "DISMISS_TOAST": { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { useToast, toast }; diff --git a/examples/swaps-api-nextjs-sui/src/images/logos/cnn.svg b/examples/swaps-api-nextjs-sui/src/images/logos/cnn.svg new file mode 100644 index 0000000..a640727 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/images/logos/cnn.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/examples/swaps-api-nextjs-sui/src/images/logos/forbes.svg b/examples/swaps-api-nextjs-sui/src/images/logos/forbes.svg new file mode 100644 index 0000000..f3929e8 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/images/logos/forbes.svg @@ -0,0 +1,5 @@ + + + diff --git a/examples/swaps-api-nextjs-sui/src/images/logos/sols.png b/examples/swaps-api-nextjs-sui/src/images/logos/sols.png new file mode 100644 index 0000000..38664dc Binary files /dev/null and b/examples/swaps-api-nextjs-sui/src/images/logos/sols.png differ diff --git a/examples/swaps-api-nextjs-sui/src/images/logos/techcrunch.svg b/examples/swaps-api-nextjs-sui/src/images/logos/techcrunch.svg new file mode 100644 index 0000000..de91f48 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/images/logos/techcrunch.svg @@ -0,0 +1,5 @@ + + + diff --git a/examples/swaps-api-nextjs-sui/src/images/logos/wired.svg b/examples/swaps-api-nextjs-sui/src/images/logos/wired.svg new file mode 100644 index 0000000..e76e50b --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/images/logos/wired.svg @@ -0,0 +1,5 @@ + + + diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/allowance.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/allowance.interface.ts new file mode 100644 index 0000000..1493f6f --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/allowance.interface.ts @@ -0,0 +1,15 @@ +export interface AllowanceQueryParams { + bridge: string; + fromAddress: string; + fromChain: string; + toChain: string; + tokenAddress: string; + tokenSymbol: string; + toTokenSymbol: string; + toTokenAddress: string; + contractCall: boolean; +} + +export interface AllowanceAPIResponse { + allowance: string; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/approval.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/approval.interface.ts new file mode 100644 index 0000000..bf0b00e --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/approval.interface.ts @@ -0,0 +1,33 @@ +export interface ApprovalTxDataQueryParams { + bridge: string; + fromAddress: string; + fromChain: string; + toChain: string; + tokenAddress: string; + tokenSymbol: string; + tokenAmount: string; + toTokenSymbol: string; + toTokenAddress: string; + contractCall: boolean; +} + +export interface ApprovalTxDataAPIResponse { + tx?: TransactionData[] | undefined; + fromChain: Chain | undefined; +} + +export interface TransactionData { + data: string; + to: string; + value?: string | undefined; + gas?: string | undefined; + from: string; + nonce?: number | undefined; +} + +interface Chain { + chainId: number; + name?: string | undefined; + slug: string; + protocolType: "evm" | "ibc" | "solana" | "multiversx"; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/chain.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/chain.interface.ts new file mode 100644 index 0000000..213fbc0 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/chain.interface.ts @@ -0,0 +1,24 @@ +export interface Chain { + id: string; + slug: string; + name: string; + logo: string; + type: "solana" | "evm" | "bitcoin" | "ibc" | "multiversx" | "tron" | "ton"; + singleChainSwap: boolean; + singleChainStaking: boolean; + txExplorer?: string | undefined; + tokenExplorer?: string | undefined; + rpcUrl?: string | undefined; + nativeToken?: { + symbol: string; + decimals: number; + logo: string; + address: string; + chain: string; + }; +} + +export interface ChainsQueryParams { + integration?: string | undefined; + type?: "evm" | "ibc" | "solana" | "multiversx" | "bitcoin" | undefined; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/history.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/history.interface.ts new file mode 100644 index 0000000..424bd5d --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/history.interface.ts @@ -0,0 +1,61 @@ +export interface Transaction { + type?: + | "swap" + | "deposit" + | "withdraw" + | "claim" + | "custom_contract" + | null + | undefined; + status: + | "Submitted" + | "Pending Source Chain" + | "Pending Destination Chain" + | "Completed" + | "Refund Required" + | "Refunded" + | "Failed Source Chain" + | "Failed Destination Chain" + | "Fallback" + | "Not Sent" + | "Claim Required"; + reason?: string; + bridge?: string | undefined; + txId?: string | undefined; + integration?: string; + needClaim?: boolean; + refundReason?: string; + errorReason?: string; + fromTokenAddress?: string; + fromChainId?: number; + fromChainSlug?: string; + fromAmount?: string; + fromAmountUsdValue?: string; + toTokenAddress?: string; + toChainId?: number; + toChainSlug?: string; + toAmount?: string; + toAmountUsdValue?: string; + fromChainTxHash?: string; + toChainTxHash?: string; + fromTokenSymbol?: string; + toTokenSymbol?: string; + fromUserAddress?: string; + toUserAddress?: string; + txStartedTimestamp?: number; + txCompletedTimestamp?: number; + updatedAt?: string; + createdAt?: string; + fallbackTokenAddress?: string; + fallbackAmount?: string; + id?: number; + projectId?: string; +} + +export interface TransactionResponseAPIResponse { + transactions?: Transaction[] | undefined; +} + +export interface TransactionQueryParams { + userAddress: string; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/quote.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/quote.interface.ts new file mode 100644 index 0000000..ad1ac1b --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/quote.interface.ts @@ -0,0 +1,102 @@ +export interface QuoteQueryParams { + tokenSymbol: string; + toTokenSymbol: string; + tokenAmount: string; + fromTokenAddress: string; + fromChain: string; + toChain: string; + fromUserAddress: string; + maxSlippage?: number; + toUserAddress: string; + partner?: string; + projectId?: string; + toTokenAddress: string; + debug?: string; + contractCall?: boolean; + skipGasEstimate?: boolean; + fee?: number; + nativeStaking?: boolean; +} + +export interface QuoteAPIResponse { + routes: Route[]; + fromToken: Token; + fromChain: Chain; + toToken: Token; + toChain: Chain; +} + +export interface Route { + route: RouteStep[]; + quote: Quote; + duration: number; + gas: string; + distribution?: { + [key: string]: number; + }; + gasUSD: string; +} + +export interface RouteStep { + bridge: string; + bridgeTokenAddress: string; + steps: ( + | "allowance" + | "approve" + | "send" + | "nativeStaking" + | "sign" + | "claim" + | "bridge" + )[]; + name: string; + part: number; + encryptionKeyRequired?: boolean | undefined; +} + +interface Quote { + bridgeFeeInNativeToken: string; + bridgeFee: string; + integration: string; + type: "swap" | "custom_contract" | "deposit" | "withdraw" | "claim" | null; + fromAmount?: string; + amount: string; + decimals: number; + amountUSD: string; + bridgeFeeUSD: string; + bridgeFeeInNativeTokenUSD: string; + fees: Fee[]; +} + +interface Fee { + type: "bridge" | "gas" | "partner"; + amount: string; + amountUSD: string; + tokenSymbol: string; + tokenAddress: string; + chainSlug: string; + decimals: number; + deductedFromSourceToken: boolean; +} + +interface Token { + symbol: string; + name?: string | undefined; + address: string; + decimals: number; + chainId?: number | undefined; + chain?: string | undefined; + logoURI?: string | undefined; +} + +interface Chain { + chainId: number; + name?: string; + slug: string; + protocolType: "evm" | "ibc" | "solana" | "multiversx"; + logo?: string; + isSingleChainSupported?: boolean; + blockExploreUrls?: string[]; + tokenExplorer?: string; + txExplorer?: string; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/send.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/send.interface.ts new file mode 100644 index 0000000..4d1e315 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/send.interface.ts @@ -0,0 +1,76 @@ +export const pendingStatuses = [ + "Submitted", + "Not Sent", + "Pending Source Chain", + "Pending Destination Chain", +]; + +export interface SendTransactionApiResponse { + id: number; + fromToken: Token; + toToken: Token; + fromChain: Chain; + toChain: Chain; + route: Route[]; + tx?: TransactionDetails; +} + +interface Token { + symbol: string; + name?: string | undefined; + address: string; + decimals: number; + chainId?: number | undefined; + chain?: string | undefined; + logoURI?: string | undefined; +} + +interface Chain { + chainId: number; + name?: string; + slug: string; + protocolType: string; +} + +export interface TransactionDetails { + from: string; + to: string; + data: string; + value?: string; + nonce?: number; + txId?: string; + gas?: string; +} + +export interface SendTransactionPayload { + fromUserAddress: string; + toUserAddress: string; + tokenSymbol: string; + fromTokenAddress: string; + fromChain: string; + toTokenSymbol: string; + toTokenAddress: string; + toChain: string; + tokenAmount: string; + toTokenAmount: string; + route: Route[]; + projectId?: string; + type?: string; + integration?: string; +} + +interface Route { + bridge: string; + bridgeTokenAddress: string; + steps: ( + | "allowance" + | "approve" + | "send" + | "nativeStaking" + | "sign" + | "claim" + | "bridge" + )[]; + name: string; + part: number; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/status.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/status.interface.ts new file mode 100644 index 0000000..4a204aa --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/status.interface.ts @@ -0,0 +1,41 @@ +export interface TransactionStatusAPIResponse { + type?: "swap" | "approve" | "sign" | "claim" | "deposit" | "withdraw"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + status: "Pending" | "Success" | "Failed" | any; + reason?: string; + bridge?: string | undefined; + txId?: string | undefined; + integration?: string; + needClaim?: boolean; + refundReason?: string; + errorReason?: string; + fromTokenAddress?: string; + fromChainId?: number; + fromChainSlug?: string; + fromAmount?: string; + fromAmountUsdValue?: string; + toTokenAddress?: string; + toChainId?: number; + toChainSlug?: string; + toAmount?: string; + toAmountUsdValue?: string; + fromChainTxHash?: string; + toChainTxHash?: string; + fromTokenSymbol?: string; + toTokenSymbol?: string; + fromUserAddress?: string; + toUserAddress?: string; + txStartedTimestamp?: number; + txCompletedTimestamp?: number; + updatedAt?: string; + createdAt?: string; + fallbackTokenAddress?: string; + fallbackAmount?: string; + id?: string; + projectId?: string; +} + +export interface TransactionStatusParams { + id: string; + txHash?: string; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/swing-service.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/swing-service.interface.ts new file mode 100644 index 0000000..940e52f --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/swing-service.interface.ts @@ -0,0 +1,48 @@ +import { + AllowanceQueryParams, + AllowanceAPIResponse, +} from "./allowance.interface"; +import { + ApprovalTxDataQueryParams, + ApprovalTxDataAPIResponse, +} from "./approval.interface"; +import { ChainsQueryParams, Chain } from "./chain.interface"; +import { + TransactionQueryParams, + TransactionResponseAPIResponse, +} from "./history.interface"; +import { QuoteAPIResponse, QuoteQueryParams } from "./quote.interface"; +import { + SendTransactionPayload, + SendTransactionApiResponse, +} from "./send.interface"; +import { + TransactionStatusParams, + TransactionStatusAPIResponse, +} from "./status.interface"; +import { TokenQueryParams, Token } from "./token.interface"; + +export interface ISwingServiceAPI { + getQuoteRequest( + queryParams: QuoteQueryParams, + ): Promise; + getAllowanceRequest( + queryParams: AllowanceQueryParams, + ): Promise; + getApprovalTxDataRequest( + queryParams: ApprovalTxDataQueryParams, + ): Promise; + getChainsRequest( + queryParams: ChainsQueryParams, + ): Promise; + getTokensRequest(queryParams: TokenQueryParams): Promise; + getTransationHistoryRequest( + queryParams: TransactionQueryParams, + ): Promise; + getTransationStatusRequest( + queryParams: TransactionStatusParams, + ): Promise; + sendTransactionRequest( + payload: SendTransactionPayload, + ): Promise; +} diff --git a/examples/swaps-api-nextjs-sui/src/interfaces/token.interface.ts b/examples/swaps-api-nextjs-sui/src/interfaces/token.interface.ts new file mode 100644 index 0000000..55a059a --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/interfaces/token.interface.ts @@ -0,0 +1,11 @@ +export interface TokenQueryParams { + chain: string; +} + +export interface Token { + symbol: string; + address: string; + logo: string; + decimals: number; + chain: string; +} diff --git a/examples/swaps-api-nextjs-sui/src/lib.ts b/examples/swaps-api-nextjs-sui/src/lib.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/lib.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/examples/swaps-api-nextjs-sui/src/services/requests.ts b/examples/swaps-api-nextjs-sui/src/services/requests.ts new file mode 100644 index 0000000..9d5813d --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/services/requests.ts @@ -0,0 +1,204 @@ +import { SwingSDK } from "@swing.xyz/sdk"; +import { + AllowanceAPIResponse, + AllowanceQueryParams, +} from "interfaces/allowance.interface"; +import { + ApprovalTxDataAPIResponse, + ApprovalTxDataQueryParams, +} from "interfaces/approval.interface"; +import { Chain, ChainsQueryParams } from "interfaces/chain.interface"; +import { + TransactionQueryParams, + TransactionResponseAPIResponse, +} from "interfaces/history.interface"; +import { QuoteAPIResponse, QuoteQueryParams } from "interfaces/quote.interface"; +import { + SendTransactionApiResponse, + SendTransactionPayload, +} from "interfaces/send.interface"; +import { + TransactionStatusAPIResponse, + TransactionStatusParams, +} from "interfaces/status.interface"; +import { ISwingServiceAPI } from "interfaces/swing-service.interface"; +import { Token, TokenQueryParams } from "interfaces/token.interface"; + +const projectId = "replug"; + +export class SwingServiceAPI implements ISwingServiceAPI { + private readonly swingSDK: SwingSDK; + + constructor() { + this.swingSDK = new SwingSDK({ + projectId: "replug", + debug: true, + }); + } + + async getQuoteRequest( + queryParams: QuoteQueryParams, + ): Promise { + try { + const response = await this.swingSDK.crossChainAPI.GET( + "/v0/transfer/quote", + { + params: { + query: queryParams, + }, + }, + ); + return response.data; + } catch (error) { + console.error("Error fetching quote:", error); + throw error; + } + } + + async getAllowanceRequest( + queryParams: AllowanceQueryParams, + ): Promise { + try { + const response = await this.swingSDK.crossChainAPI.GET( + "/v0/transfer/allowance", + { + params: { + query: queryParams, + }, + }, + ); + + return response.data; + } catch (error) { + console.error("Error fetching allowance:", error); + throw error; + } + } + + async getApprovalTxDataRequest( + queryParams: ApprovalTxDataQueryParams, + ): Promise { + try { + const response = await this.swingSDK.crossChainAPI.GET( + "/v0/transfer/approve", + { + params: { + query: queryParams, + }, + }, + ); + return response.data; + } catch (error) { + console.error("Error fetching approval:", error); + throw error; + } + } + + async getChainsRequest( + queryParams: ChainsQueryParams, + ): Promise { + try { + const response = await this.swingSDK.platformAPI.GET("/chains", { + params: { + query: queryParams, + }, + }); + return response.data; + } catch (error) { + console.error("Error fetching approval:", error); + throw error; + } + } + + async getTokensRequest( + queryParams: TokenQueryParams, + ): Promise { + try { + const response = await this.swingSDK.platformAPI.GET("/tokens", { + params: { + query: queryParams, + }, + }); + return response.data; + } catch (error) { + console.error("Error fetching approval:", error); + throw error; + } + } + + async getTransationHistoryRequest( + queryParams: TransactionQueryParams, + ): Promise { + try { + const response = await this.swingSDK.crossChainAPI.GET( + "/v0/transfer/history", + { + params: { + query: queryParams, + }, + }, + ); + + return response.data; + } catch (error) { + console.error("Error fetching transaction status:", error); + + throw error; + } + } + + async getTransationStatusRequest( + queryParams: TransactionStatusParams, + ): Promise { + try { + const response = await this.swingSDK.platformAPI.GET( + "/projects/{projectId}/transactions/{transactionId}", + { + params: { + path: { + transactionId: queryParams.id, + projectId, + }, + query: { + txHash: queryParams.txHash, + }, + }, + }, + ); + + return response.data; + } catch (error) { + console.error("Error fetching transaction status:", error); + throw error; + } + } + + async sendTransactionRequest( + payload: SendTransactionPayload, + ): Promise { + try { + const response = await this.swingSDK.crossChainAPI.POST( + "/v0/transfer/send", + { + body: payload, + }, + ); + return response.data; + } catch (error) { + console.error("Error sending transaction:", error); + throw error; + } + } + + get isSDKConnected() { + return this.swingSDK.isReady; + } + + get swingSdk(): null | this { + if (this.isSDKConnected) { + return this; + } + + return null; + } +} diff --git a/examples/swaps-api-nextjs-sui/src/styles/globals.css b/examples/swaps-api-nextjs-sui/src/styles/globals.css new file mode 100644 index 0000000..3716aab --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/styles/globals.css @@ -0,0 +1,16 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + --tw-bg-opacity: 1; + background-color: rgb(15 23 42 / var(--tw-bg-opacity)); +} + +@layer base { + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } +} \ No newline at end of file diff --git a/examples/swaps-api-nextjs-sui/src/types/global.d.ts b/examples/swaps-api-nextjs-sui/src/types/global.d.ts new file mode 100644 index 0000000..b050294 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/types/global.d.ts @@ -0,0 +1,22 @@ +interface SolanaProvider { + isPhantom: boolean; + publicKey: { + toString(): string; + }; + connect: (args?: { + onlyIfTrusted: boolean; + }) => Promise<{ publicKey: { toString(): string } }>; + disconnect: () => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + on: (event: string, handler: (args: any) => void) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: (method: string, params: any) => Promise; + signTransaction: (transaction: Transaction) => Promise; + signAndSendTransaction: ( + transaction: Transaction, + ) => Promise<{ signature: string }>; +} + +interface Window { + solana?: SolanaProvider; +} diff --git a/examples/swaps-api-nextjs-sui/src/types/transfer.types.ts b/examples/swaps-api-nextjs-sui/src/types/transfer.types.ts new file mode 100644 index 0000000..c6b1109 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/types/transfer.types.ts @@ -0,0 +1,29 @@ +import { QuoteQueryParams } from "interfaces/quote.interface"; + +interface ChainDecimals { + fromChainDecimal?: number; + toChainDecimal?: number; +} + +interface ChainIcons { + fromChainIconUrl?: string; + toChainIconUrl?: string; + fromTokenIconUrl?: string; + toTokenIconUrl?: string; +} + +interface NativeSourceToken { + fromNativeTokenSymbol?: string; + toNativeTokenSymbol?: string; +} + +interface ChainIds { + fromChainId?: string; + toChainId?: string; +} + +export type TransferParams = QuoteQueryParams & + ChainDecimals & + ChainIcons & + NativeSourceToken & + ChainIds; diff --git a/examples/swaps-api-nextjs-sui/src/utils/ethToWei.ts b/examples/swaps-api-nextjs-sui/src/utils/ethToWei.ts new file mode 100644 index 0000000..d2e6101 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/src/utils/ethToWei.ts @@ -0,0 +1,19 @@ +import { ethers } from "ethers"; + +/** + * Converts ETH to Wei. + * + * @param {string | number} ethAmount The amount in ETH to convert. + * @returns {string} The amount in Wei as a string. + */ +export const convertEthToWei = (ethAmount: string, decimals = 18) => { + // Convert the amount to a string to handle both string and number inputs + const weiAmount = ethers.utils.parseUnits(ethAmount.toString(), decimals); + return weiAmount.toString(); +}; + +export const convertWeiToEth = (weiAmount: string, decimals = 18) => { + // Convert the amount to a string to handle both string and number inputs + const ethAmount = ethers.utils.formatUnits(weiAmount.toString(), decimals); + return Number(ethAmount.toString()).toFixed(4); +}; diff --git a/examples/swaps-api-nextjs-sui/styles/globals.css b/examples/swaps-api-nextjs-sui/styles/globals.css new file mode 100644 index 0000000..91f55f0 --- /dev/null +++ b/examples/swaps-api-nextjs-sui/styles/globals.css @@ -0,0 +1,4 @@ +@tailwind base; + @tailwind components; + @tailwind utilities; + \ No newline at end of file diff --git a/examples/swaps-api-nextjs-sui/tailwind.config.js b/examples/swaps-api-nextjs-sui/tailwind.config.js new file mode 100644 index 0000000..afa115d --- /dev/null +++ b/examples/swaps-api-nextjs-sui/tailwind.config.js @@ -0,0 +1,127 @@ +const defaultTheme = require('tailwindcss/defaultTheme'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ['class'], + content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'], + prefix: '', + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], + sm: ['0.875rem', { lineHeight: '1.5rem' }], + base: ['1rem', { lineHeight: '1.5rem' }], + lg: ['1.125rem', { lineHeight: '2rem' }], + xl: ['1.25rem', { lineHeight: '1.75rem' }], + '2xl': ['1.5rem', { lineHeight: '2rem' }], + '3xl': ['2rem', { lineHeight: '3rem' }], + '4xl': ['2.5rem', { lineHeight: '3rem' }], + '5xl': ['3rem', { lineHeight: '1' }], + '6xl': ['3.75rem', { lineHeight: '1' }], + '7xl': ['4.5rem', { lineHeight: '1' }], + '8xl': ['6rem', { lineHeight: '1' }], + '9xl': ['8rem', { lineHeight: '1' }], + }, + extend: { + colors: ({ colors }) => ({ + gray: colors.neutral, + }), + fontFamily: { + sans: ['Inter', ...defaultTheme.fontFamily.sans], + }, + keyframes: { + 'fade-in': { + from: { + opacity: 0, + }, + to: { + opacity: 1, + }, + }, + marquee: { + '100%': { + transform: 'translateY(-50%)', + }, + }, + 'spin-reverse': { + to: { + transform: 'rotate(-360deg)', + }, + }, + }, + maxWidth: { + '2xl': '40rem', + }, + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + '4xl': '2rem', + '5xl': '2.5rem', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'fade-in': 'fade-in 0.5s linear forwards', + marquee: 'marquee var(--marquee-duration) linear infinite', + 'spin-slow': 'spin 4s linear infinite', + 'spin-slower': 'spin 6s linear infinite', + 'spin-reverse': 'spin-reverse 1s linear infinite', + 'spin-reverse-slow': 'spin-reverse 4s linear infinite', + 'spin-reverse-slower': 'spin-reverse 6s linear infinite', + }, + }, + }, + plugins: [require('tailwindcss-animate'), require('@tailwindcss/forms')], +}; diff --git a/examples/swaps-api-nextjs-sui/tsconfig.json b/examples/swaps-api-nextjs-sui/tsconfig.json new file mode 100644 index 0000000..0bf84cb --- /dev/null +++ b/examples/swaps-api-nextjs-sui/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "typescript-config/nextjs.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": "./src", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/swaps-api-nextjs-sui/turbo.json b/examples/swaps-api-nextjs-sui/turbo.json new file mode 100644 index 0000000..1dbe6de --- /dev/null +++ b/examples/swaps-api-nextjs-sui/turbo.json @@ -0,0 +1,11 @@ +{ + "extends": ["//"], + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "outputs": [".next/**", "!.next/cache/**"], + "env": ["NEXT_PUBLIC_THIRD_WEB_CLIENT_ID", "THIRD_WEB_SECRET_KEY"], + "outputMode": "new-only" + } + } +} diff --git a/examples/swaps-api-nextjs-ton/src/components/ui/Header.tsx b/examples/swaps-api-nextjs-ton/src/components/ui/Header.tsx index 140f282..e413e31 100644 --- a/examples/swaps-api-nextjs-ton/src/components/ui/Header.tsx +++ b/examples/swaps-api-nextjs-ton/src/components/ui/Header.tsx @@ -142,7 +142,7 @@ export function Header() {