diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/examples/staking-sdk-nextjs/src/components/Stake.tsx b/examples/staking-sdk-nextjs/src/components/Stake.tsx index 8a0e73a..f4056c3 100644 --- a/examples/staking-sdk-nextjs/src/components/Stake.tsx +++ b/examples/staking-sdk-nextjs/src/components/Stake.tsx @@ -154,6 +154,36 @@ export function Stake() { > Stake Now + ); }) diff --git a/examples/swaps-api-nextjs-evm-gasless/.eslintrc.js b/examples/swaps-api-nextjs-evm-gasless/.eslintrc.js new file mode 100644 index 0000000..ea43de2 --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/.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-evm-gasless/.gitignore b/examples/swaps-api-nextjs-evm-gasless/.gitignore new file mode 100644 index 0000000..55175ef --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/.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-evm-gasless/README.md b/examples/swaps-api-nextjs-evm-gasless/README.md new file mode 100644 index 0000000..0598d33 --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/README.md @@ -0,0 +1,370 @@ +# Gasless Cross-chain Swaps using the Swing API in Next.js + +This example demonstrates how to perform **gasless cross-chain transactions** using Swing's API, built with: + +- [@thirdweb-dev/react](https://portal.thirdweb.com/react) +- [@thirdweb-dev/sdk](https://portal.thirdweb.com/typescript) +- [Next.js App Router](https://nextjs.org) +- [Tailwind CSS](https://tailwindcss.com) + +## Demo + +View the live demo [https://swaps-api-nextjs-evm-gasless.vercel.app](https://swaps-api-nextjs-evm-gasless.vercel.app/) + +## Gasless Transactions Overview + +Gasless transactions allow users to perform cross-chain swaps without paying gas fees upfront. Instead of sending a traditional transaction, users sign an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data message that gets executed on their behalf. + +For more detailed information about gasless transactions, visit the [Swing Gasless Transactions Documentation](https://docs-git-feat-gasless-trans-update-swing-xyz.vercel.app/gasless-transactions). + +### Key Benefits: + +- No upfront gas fees for users +- Enhanced user experience +- Seamless cross-chain swaps +- EIP-712 signature-based execution + +## Swing Integration + +> The implementation of Swing's [Cross-chain API](https://developers.swing.xyz/reference/api) for gasless transactions can be found in [src/components/Swap.tsx](./src/components/Swap.tsx). + +This example demonstrates how you can perform gasless cross-chain transactions between different chains. The process for performing a gasless transaction includes: + +- Getting a [gasless quote](https://developers.swing.xyz/reference/api/cross-chain/1169f8cbb6937-request-a-transfer-quote) with `mode: "gasless"` +- Checking token allowance (if needed) +- Approving token spending (if needed) +- Sending a [gasless transaction](https://developers.swing.xyz/reference/api/cross-chain/d83d0d65028dc-send-transfer) with EIP-712 signature + +> For gasless transactions, real-time updates via the [status](https://developers.swing.xyz/reference/api/cross-chain/6b61efd1b798a-transfer-status) endpoint are essential for tracking transaction progress. + +## 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-evm-gasless +``` + +Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. + +## Getting a Gasless Quote + +To perform a gasless swap, we first need to get a gasless quote from Swing's Cross-Chain API by including the **`mode: "gasless"`** parameter. + +URL: [https://swap.prod.swing.xyz/v0/transfer/quote](https://swap.prod.swing.xyz/v0/transfer/quote) + +**Parameters**: + +| Property | Example | Description | +| ------------------ | -------------------------------------------- | ------------------------------------------------------- | +| `tokenAmount` | 1000000000000000000 | Amount of the source token being sent (in wei for ETH). | +| `fromChain` | `base` | Source Chain slug | +| `fromUserAddress` | 0x018c15DA1239B84b08283799B89045CD476BBbBb | Sender's wallet address | +| `fromTokenAddress` | 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 | Source Token Address | +| `tokenSymbol` | `USDC` | Source Token slug | +| `toTokenAddress` | `0x3c499c542cef5e3811e1192ce70d8cc03d5c3359` | Destination Token Address | +| `toTokenSymbol` | `USDC` | Destination Token slug | +| `toChain` | `polygon` | Destination Chain slug | +| `toUserAddress` | 0x018c15DA1239B84b08283799B89045CD476BBbBb | Receiver's wallet address | +| `mode` | `gasless` | **Required for gasless transactions** | +| `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 gasless quote called `getQuoteRequest()`: + +```typescript +export const getQuoteRequest = async ( + queryParams: QuoteQueryParams, +): Promise => { + try { + const response = await axios.get( + `${baseUrl}/transfer/quote`, + { params: { ...queryParams, projectId, mode: "gasless", debug: true } }, + ); + return response.data; + } catch (error) { + console.error("Error fetching quote:", error); + throw error; + } +}; +``` + +The response provides gasless-compatible routes with fee structures optimized for signature-based execution. + +## Allowance and Approval (ERC-20 Tokens) + +For ERC-20 tokens, you may need to check and approve token spending before executing the gasless transaction. + +### Check Allowance + +URL: [https://swap.prod.swing.xyz/v0/transfer/allowance](https://swap.prod.swing.xyz/v0/transfer/allowance) + +```typescript +export const getAllowanceRequest = async ( + queryParams: AllowanceQueryParams, +): Promise => { + try { + const response = await axios.get( + `${baseUrl}/transfer/allowance`, + { params: { ...queryParams, projectId } }, + ); + return response.data; + } catch (error) { + console.error("Error fetching allowance:", error); + throw error; + } +}; +``` + +### Get Approval Transaction + +URL: [https://swap.prod.swing.xyz/v0/transfer/approve](https://swap.prod.swing.xyz/v0/transfer/approve) + +```typescript +export const getApproveRequest = async ( + queryParams: ApproveQueryParams, +): Promise => { + try { + const response = await axios.get( + `${baseUrl}/transfer/approve`, + { params: { ...queryParams, projectId } }, + ); + return response.data; + } catch (error) { + console.error("Error fetching approve transaction:", error); + throw error; + } +}; +``` + +## Sending a Gasless Transaction + +After getting a gasless quote and handling approvals, you'll send a gasless transaction to Swing's API. + +### Making a `/send` Request for Gasless + +The `/send` endpoint for gasless transactions returns EIP-712 typed data instead of regular transaction data: + +```typescript +export const sendTransactionRequest = async ( + payload: SendTransactionPayload, +): Promise => { + try { + const response = await axios.post( + `${baseUrl}/transfer/send`, + { ...payload, projectId, mode: "gasless" }, + { + headers: { + "Content-Type": "application/json", + }, + }, + ); + return response.data; + } catch (error) { + console.error("Error sending transaction:", error); + throw error; + } +}; +``` + +### EIP-712 Signature Flow + +For gasless transactions, instead of sending a regular transaction, you sign EIP-712 typed data: + +```typescript +// src/components/Swap.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: tokenAmount, + route: transferRoute.route, + type: "swap", +}); + +if (transfer?.tx.meta) { + // For gasless transactions, extract EIP-712 data from meta + const tx = JSON.stringify(transfer.tx); + const txInfo = JSON.parse(tx); + const txObj = txInfo.meta; + + const JAM_DOMAIN = txObj.domain; + const JAM_ORDER_TYPES = txObj.types; + const toSign = txObj.value; + + const types = { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + ...JAM_ORDER_TYPES, + }; + + const message = toSign; + const msgParams = JSON.stringify({ + domain: JAM_DOMAIN, + primaryType: Object.keys(JAM_ORDER_TYPES)[0], + message, + types, + }); + + // Sign the EIP-712 typed data + const account = (window as any).ethereum.selectedAddress; + const signature = await(window as any).ethereum.request({ + method: "eth_signTypedData_v4", + params: [account, msgParams], + }); + + console.log("EIP-712 Signature:", signature); + + // Poll transaction status with the signature + pollTransactionStatus(transfer.id.toString(), signature); +} +``` + +## Complete Gasless Transaction Flow + +Here's the complete flow for a gasless transaction: + +```typescript +async function startTransfer() { + // 1. Switch to correct chain + const chainIdMap = { + polygon: "0x89", + base: "0x2105", + }; + + await window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: chainIdMap[transferParams.fromChain] }], + }); + + // 2. Check allowance (for ERC-20 tokens) + 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, + }); + + // 3. Approve if needed + if (Number(checkAllowance?.allowance || "0") <= 0) { + const getApprovalTxData = await getApproveRequest({ + 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 = { + data: getApprovalTxData?.tx?.at(0)?.data!, + from: getApprovalTxData?.tx?.at(0)?.from!, + to: getApprovalTxData?.tx?.at(0)?.to!, + }; + + const txResponse = await signer?.sendTransaction(txData); + await txResponse?.wait(); + } + + // 4. Execute gasless transaction with EIP-712 signature + const transfer = await sendTransactionRequest({ + // ... transaction parameters + mode: "gasless", + }); + + // 5. Sign EIP-712 typed data (as shown above) + // 6. Poll for transaction status +} +``` + +## API Restrictions for Gasless Transactions + +When using gasless transactions, be aware of the following API restrictions: + +- **Mode Parameter**: Always include `mode: "gasless"` in quote and send requests +- **EIP-712 Signatures**: Only EIP-712 signatures are supported, not regular transactions +- **Chain Support**: Gasless transactions are available on supported EVM chains +- **Token Approvals**: Standard ERC-20 approvals may still be required before gasless execution + +## Polling Transaction Status + +After signing the EIP-712 typed data, poll the transaction status using the signature: + +```typescript +async function pollTransactionStatus(transId: string, signature: string) { + const transactionStatus = await getTransationStatus({ + id: transId, + txHash: signature, // Use signature as txHash for gasless transactions + }); + + setTransStatus(transactionStatus); + + if (pendingStatuses.includes(transactionStatus?.status)) { + setTimeout( + () => pollTransactionStatus(transId, signature), + transactionPollingDuration, + ); + } else { + setTransferRoute(null); + toast({ + title: "Transaction Successful", + description: "Gasless bridge completed successfully", + }); + } +} +``` + +## Default Configuration + +Here's the default configuration for Base → Polygon USDC gasless transfers: + +```typescript +const defaultTransferParams: TranferParams = { + tokenAmount: "1", + fromChain: "base", + fromUserAddress: "0xE1e0992Be9902E92460AC0Ff625Dcc1c485FCF6b", + fromTokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + fromTokenIconUrl: + "https://raw.githubusercontent.com/Pymmdrza/Cryptocurrency_Logos/mainx/PNG/usdc.png", + fromChainDecimal: 6, + tokenSymbol: "USDC", + toTokenAddress: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + toTokenSymbol: "USDC", + toChain: "polygon", + toTokenIconUrl: + "https://raw.githubusercontent.com/Pymmdrza/Cryptocurrency_Logos/mainx/PNG/usdc.png", + toUserAddress: "0xE1e0992Be9902E92460AC0Ff625Dcc1c485FCF6b", + toChainDecimal: 6, +}; +``` + +## 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. + +For more information about gasless transactions and advanced configuration, visit the [Swing Gasless Documentation](https://docs-git-feat-gasless-trans-update-swing-xyz.vercel.app/gasless-transactions). diff --git a/examples/swaps-api-nextjs-evm-gasless/components.json b/examples/swaps-api-nextjs-evm-gasless/components.json new file mode 100644 index 0000000..44d0040 --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/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-evm-gasless/next-env.d.ts b/examples/swaps-api-nextjs-evm-gasless/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/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-evm-gasless/next.config.mjs b/examples/swaps-api-nextjs-evm-gasless/next.config.mjs new file mode 100644 index 0000000..d5456a1 --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/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-evm-gasless/package.json b/examples/swaps-api-nextjs-evm-gasless/package.json new file mode 100644 index 0000000..dfed2a8 --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/package.json @@ -0,0 +1,58 @@ +{ + "name": "swaps-api-nextjs-evm-gasless", + "demo": "https://swaps-api-nextjs-evm-gasless.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", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-toast": "^1.1.5", + "@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", + "thirdweb": "^5.105.37", + "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-evm-gasless/postcss.config.js b/examples/swaps-api-nextjs-evm-gasless/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/swaps-api-nextjs-evm-gasless/public/favicon.ico b/examples/swaps-api-nextjs-evm-gasless/public/favicon.ico new file mode 100644 index 0000000..88b1f7a Binary files /dev/null and b/examples/swaps-api-nextjs-evm-gasless/public/favicon.ico differ diff --git a/examples/swaps-api-nextjs-evm-gasless/public/fonts/Inter-italic.var.woff2 b/examples/swaps-api-nextjs-evm-gasless/public/fonts/Inter-italic.var.woff2 new file mode 100644 index 0000000..b826d5a Binary files /dev/null and b/examples/swaps-api-nextjs-evm-gasless/public/fonts/Inter-italic.var.woff2 differ diff --git a/examples/swaps-api-nextjs-evm-gasless/public/fonts/Inter-roman.var.woff2 b/examples/swaps-api-nextjs-evm-gasless/public/fonts/Inter-roman.var.woff2 new file mode 100644 index 0000000..6a256a0 Binary files /dev/null and b/examples/swaps-api-nextjs-evm-gasless/public/fonts/Inter-roman.var.woff2 differ diff --git a/examples/swaps-api-nextjs-evm-gasless/src/app/layout.tsx b/examples/swaps-api-nextjs-evm-gasless/src/app/layout.tsx new file mode 100644 index 0000000..cbbe21e --- /dev/null +++ b/examples/swaps-api-nextjs-evm-gasless/src/app/layout.tsx @@ -0,0 +1,23 @@ +import "styles/globals.css"; +import "@fortawesome/fontawesome-svg-core/styles.css"; + +import { Header } from "../components/ui/Header"; +import { Footer } from "../components/ui/Footer"; + +import { Toaster } from "components/ui/toaster"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + +
+ +
{children}
+ +