From c2f8ac465bd4af31c2aa6f85106f6eefba010338 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Fri, 13 Feb 2026 18:27:44 +0200 Subject: [PATCH 01/12] feat(docs): add guides for gasless FXRP payments --- docs/fxrp/overview.mdx | 6 + ...0-fxrp-address.mdx => 01-fxrp-address.mdx} | 0 ...0-fxrp-swap.mdx => 02-usdt0-fxrp-swap.mdx} | 0 ...x402-payments.mdx => 03-x402-payments.mdx} | 0 .../04-gasless-fxrp-payments.mdx | 255 +++++++++++++++++ docs/tags.yml | 4 + .../fxrp-gasless/deploy.ts | 51 ++++ .../fxrp-gasless/example-usage.ts | 70 +++++ .../fxrp-gasless/payment.ts | 239 ++++++++++++++++ .../fxrp-gasless/relayer.ts | 257 ++++++++++++++++++ sidebars.ts | 1 + 11 files changed, 883 insertions(+) rename docs/fxrp/token-interactions/{0-fxrp-address.mdx => 01-fxrp-address.mdx} (100%) rename docs/fxrp/token-interactions/{1-usdt0-fxrp-swap.mdx => 02-usdt0-fxrp-swap.mdx} (100%) rename docs/fxrp/token-interactions/{2-x402-payments.mdx => 03-x402-payments.mdx} (100%) create mode 100644 docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx create mode 100644 examples/developer-hub-javascript/fxrp-gasless/deploy.ts create mode 100644 examples/developer-hub-javascript/fxrp-gasless/example-usage.ts create mode 100644 examples/developer-hub-javascript/fxrp-gasless/payment.ts create mode 100644 examples/developer-hub-javascript/fxrp-gasless/relayer.ts diff --git a/docs/fxrp/overview.mdx b/docs/fxrp/overview.mdx index bf725d2e..3bda3500 100644 --- a/docs/fxrp/overview.mdx +++ b/docs/fxrp/overview.mdx @@ -81,6 +81,12 @@ Guides for working directly with the FXRP token. href: "/fxrp/token-interactions/x402-payments", docId: "fxrp/token-interactions/x402-payments", }, + { + type: "link", + label: "Gasless FXRP Payments", + href: "/fxrp/token-interactions/gasless-fxrp-payments", + docId: "fxrp/token-interactions/gasless-fxrp-payments", + }, ]} /> diff --git a/docs/fxrp/token-interactions/0-fxrp-address.mdx b/docs/fxrp/token-interactions/01-fxrp-address.mdx similarity index 100% rename from docs/fxrp/token-interactions/0-fxrp-address.mdx rename to docs/fxrp/token-interactions/01-fxrp-address.mdx diff --git a/docs/fxrp/token-interactions/1-usdt0-fxrp-swap.mdx b/docs/fxrp/token-interactions/02-usdt0-fxrp-swap.mdx similarity index 100% rename from docs/fxrp/token-interactions/1-usdt0-fxrp-swap.mdx rename to docs/fxrp/token-interactions/02-usdt0-fxrp-swap.mdx diff --git a/docs/fxrp/token-interactions/2-x402-payments.mdx b/docs/fxrp/token-interactions/03-x402-payments.mdx similarity index 100% rename from docs/fxrp/token-interactions/2-x402-payments.mdx rename to docs/fxrp/token-interactions/03-x402-payments.mdx diff --git a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx new file mode 100644 index 00000000..42cf6b0b --- /dev/null +++ b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx @@ -0,0 +1,255 @@ +--- +title: Gasless FXRP Payments +tags: [intermediate, fassets, fxrp, meta-transactions] +slug: gasless-fxrp-payments +description: Transfer FXRP without gas using EIP-712 signed meta-transactions and a relayer. +keywords: [fassets, fxrp, gasless, meta-transactions, eip-712, flare-network, relayer] +sidebar_position: 4 +--- + +import CodeBlock from "@theme/CodeBlock"; +import GaslessPaymentForwarder from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/GaslessPaymentForwarder.sol"; +import PaymentUtils from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/payment.ts"; +import Relayer from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/relayer.ts"; +import DeployScript from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/deploy.ts"; +import ExampleUsage from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts"; + +This guide describes how to create a system for gasless FXRP (FAsset) transfers on Flare. +Users sign payment requests off-chain with [EIP-712](https://eips.ethereum.org/EIPS/eip-712), and a relayer submits them on-chain and pays gas on their behalf. + +### EIP-3009 and EIP-712 + +[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) (Transfer with Authorization) extends [ERC-20](https://eips.ethereum.org/EIPS/eip-20) with meta-transactions. The token holder signs an authorization off-chain, and a relayer executes the transfer on-chain and pays gas. +This guide uses a **custom [EIP-712](https://eips.ethereum.org/EIPS/eip-712) payment message** and a forwarder contract instead of EIP-3009 on the token itself. +The forwarder pulls FXRP from the user (after a one-time approval) and sends it to the recipient. +The relayer pays gas and can charge a fee in FXRP. + +## Project setup + +Create a new [Hardhat](https://hardhat.org/) project or use the [Flare Hardhat Starter](/network/guides/hardhat-foundry-starter-kit) kit. + +Install needed dependencies: + +```bash +npm install ethers viem express cors dotenv +npm install @openzeppelin/contracts @flarenetwork/flare-periphery-contracts +``` + +Add the following files to your project: + +### Gasless Payment Forwarder Contract + +Save as `contracts/GaslessPaymentForwarder.sol`: + +
+View GaslessPaymentForwarder.sol + + + {GaslessPaymentForwarder} + + +
+ +#### How the contract works + +1. Define libraries and state. +2. Define events. +3. Define errors. +4. Implement the contract constructor that sets the EIP-712 domain, owner, and initial relayer fee. +5. Implement the `fxrp` function that returns the FXRP token from the Flare registry. +6. Define the `executePayment` function that executes a gasless payment. +7. Hash the request (type hash + from, to, amount, fee, nonce, deadline) and get the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) digest. +8. Recover the signer from the signature with ECDSA. +9. Require signer from the payment request. +10. Require allowance is sufficient for the total amount (amount + fee). +11. Transfer amount from sender to recipient. +12. Transfer fee from sender to relayer (msg.sender); emit PaymentExecuted. +13. Define the view functions to get the nonce, domain separator, and payment request hash. +14. Define the functions to set the relayer authorization and fee. + +Run the following command to compile the contract and generate the typechain-types: + +```bash +npx hardhat compile +``` + +### Utilities + +Save as `utils/payment.ts`. + +It uses the `typechain-types` generated by the previous command. + +
+View utils/payment.ts + + + {PaymentUtils} + + +
+ +### Relayer Service + +This is a relayer service that will submit payment requests to the Flare blockchain and pay gas on behalf of the user. + +Save as `relayer/index.ts` to start the relayer service: + +
+View relayer/index.ts + + + {Relayer} + + +
+ +### Deploy Script + +Save as `scripts/deploy.ts` to deploy the [`GaslessPaymentForwarder`](/fxrp/token-interactions/gasless-fxrp-payments#gasless-payment-forwarder-contract) contract: + +
+View scripts/deploy.ts + + + {DeployScript} + + +
+ +### Example Flow + +Use `scripts/example-usage.ts` to run the example flow: + +
+View scripts/example-usage.ts + + + {ExampleUsage} + + +
+ +The example flow tests the system and executes the following steps: + +1. Check the user's balance and allowance. +2. Approve the forwarder to spend FXRP if needed. +3. Create a payment request. +4. Submit the payment request to the relayer. +5. Check the payment status. + +## Configuration + +Create a `.env` file in your project root with the following variables: + +| Variable | Description | +|----------|-------------| +| `PRIVATE_KEY` | Deployer private key (for deployment) | +| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas, receives FXRP fees) | +| `USER_PRIVATE_KEY` | User wallet for testing | +| `FORWARDER_ADDRESS` | Deployed contract address (set after deploy) | +| `RPC_URL` | Flare network RPC (e.g. Coston2 testnet) | +| `RELAYER_URL` | Relayer HTTP URL (e.g. `http://localhost:3000`) | + +## Deploy and run + +### Compile Contracts + +Run the following command to compile the contracts: + +```bash +npx hardhat compile +``` + +### Deploy the Forwarder + +Deploy the [`GaslessPaymentForwarder`](/fxrp/token-interactions/gasless-fxrp-payments#gasless-payment-forwarder-contract) contract to Coston2 (testnet): + +```bash +npx hardhat run scripts/deploy.ts --network coston2 +``` + +Set `FORWARDER_ADDRESS` in `.env` to the deployed contract address. + +### Start the Relayer + +Run the following command to start the relayer service. + +The relayer is an Express server that submits payment requests on the Flare blockchain and pays gas on behalf of the user. + +```bash +npx ts-node relayer/index.ts +``` + +**Main relayer backend endpoints:** + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/execute` | Execute single payment | +| GET | `/nonce/:address` | Get nonce for address | +| GET | `/fee` | Get the relayer fee | + +### Run the example Flow + +Run the following command to run the example: + +```bash +npx ts-node scripts/example-usage.ts +``` + +This script will execute the following steps: + +1. Check the user's balance and allowance. +2. Approve the forwarder to spend FXRP if needed. +3. Create a payment request. +4. Submit the payment request to the relayer. +5. Check the payment status. + +You should see the following output: + +
+View example output + +```console +=== FXRP Gasless Payment Example === + +User address: 0x0d09ff7630588E05E2449aBD3dDD1D8d146bc5c2 +Forwarder: 0xffc5F792e1Ca050B598577fFaFa30634A66174A5 +Relayer: http://localhost:3000 + +Step 1: Checking FXRP balance and allowance... + FXRP Token: 0x0b6A3645c240605887a5532109323A3E12273dc7 + Balance: 11773.287907 FXRP + Allowance: 0.0 FXRP + Nonce: 0 + +Step 2: Approving FXRP for gasless payments... + Approved! TX: 0x4aa5304c4e3e261d9d88ac1fe24a9d96faaa5786065a9253de463a5c33d0c368 + +Step 3: Creating payment request... + To: 0x5EEeaD99dFfeB32Fe96baad6554f6E009c8B348a + Amount: 0.1 FXRP + Fee: 0.01 FXRP + + Signed request created: + From: 0x0d09ff7630588E05E2449aBD3dDD1D8d146bc5c2 + To: 0x5EEeaD99dFfeB32Fe96baad6554f6E009c8B348a + Amount: 0.1 FXRP + Fee: 0.01 FXRP + Deadline: 2026-02-13T14:14:20.000Z + Signature: 0x0cc5532b3c67eb0ef4... + +Step 4: Submitting to relayer... + Payment executed successfully! + Transaction: 0xd18b532b294656fbeadd4884b6baf5e45efabb3809787c1625f675cccff2b648 + Block: 27196879 + Gas used: 229589 +``` + +
+ +:::tip[Next steps] +- [Get FXRP token address](/fxrp/token-interactions/fxrp-address) for use in your app. +- [Swap USDT0 to FXRP](/fxrp/token-interactions/usdt0-fxrp-swap) to acquire FXRP. +- Review [FXRP operational parameters](/fxrp/parameters). +- [Gasless USD₮0 Transfers](/network/guides/gasless-usdt0-transfers) — Similar pattern for USDT0 on Flare. +::: diff --git a/docs/tags.yml b/docs/tags.yml index f786cf85..fbe3ef48 100644 --- a/docs/tags.yml +++ b/docs/tags.yml @@ -44,6 +44,10 @@ flare-smart-accounts: label: flare-smart-accounts x402: label: x402 +fxrp: + label: fxrp +meta-transactions: + label: meta-transactions react: label: react wagmi: diff --git a/examples/developer-hub-javascript/fxrp-gasless/deploy.ts b/examples/developer-hub-javascript/fxrp-gasless/deploy.ts new file mode 100644 index 00000000..585d89f1 --- /dev/null +++ b/examples/developer-hub-javascript/fxrp-gasless/deploy.ts @@ -0,0 +1,51 @@ +/** + * Deploy GaslessPaymentForwarder + * + * Copy to scripts/deploy.ts. Usage: npx hardhat run scripts/deploy.ts --network coston2 + */ + +import hre from "hardhat"; +import { erc20Abi } from "viem"; +import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; +import type { IERC20Metadata } from "../typechain-types/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata"; + +const DEFAULT_RELAYER_FEE = 10000n; + +async function main(): Promise { + const network = hre.network.name; + console.log(`\nDeploying GaslessPaymentForwarder to ${network}...`); + + const [deployer] = await hre.ethers.getSigners(); + console.log(`Deployer address: ${deployer.address}`); + + const balance = await hre.ethers.provider.getBalance(deployer.address); + console.log(`Deployer balance: ${hre.ethers.formatEther(balance)} FLR`); + + const GaslessPaymentForwarder = await hre.ethers.getContractFactory("GaslessPaymentForwarder"); + const forwarder = (await GaslessPaymentForwarder.deploy(DEFAULT_RELAYER_FEE)) as unknown as GaslessPaymentForwarder; + + await forwarder.waitForDeployment(); + const forwarderAddress = await forwarder.getAddress(); + + const fxrpAddress = await forwarder.fxrp(); + const fxrp = new hre.ethers.Contract( + fxrpAddress, + erc20Abi as unknown as string[], + hre.ethers.provider + ) as unknown as IERC20Metadata; + const decimals = await fxrp.decimals(); + const relayerFeeFormatted = hre.ethers.formatUnits(DEFAULT_RELAYER_FEE, decimals); + + console.log(`\nGaslessPaymentForwarder deployed to: ${forwarderAddress}`); + console.log(`FXRP Token: ${fxrpAddress}`); + console.log(`Relayer Fee: ${relayerFeeFormatted} FXRP`); + + return forwarderAddress; +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts b/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts new file mode 100644 index 00000000..aa141cb5 --- /dev/null +++ b/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts @@ -0,0 +1,70 @@ +/** + * Example: full FXRP gasless payment flow + * + * Copy to scripts/example-usage.ts. Usage: npx ts-node scripts/example-usage.ts + * Prerequisites: Deploy forwarder, set FORWARDER_ADDRESS, start relayer, have FXRP. + */ + +import { Wallet, JsonRpcProvider } from "ethers"; +import { createPaymentRequest, approveFXRP, checkUserStatus } from "../utils/payment"; +import "dotenv/config"; + +const RPC_URL = process.env.RPC_URL; +const RELAYER_URL = process.env.RELAYER_URL ?? "http://localhost:3000"; +const FORWARDER_ADDRESS = process.env.FORWARDER_ADDRESS; +const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY; + +async function main(): Promise { + if (!FORWARDER_ADDRESS || !USER_PRIVATE_KEY) { + console.error("FORWARDER_ADDRESS and USER_PRIVATE_KEY required"); + process.exit(1); + } + + const provider = new JsonRpcProvider(RPC_URL); + const wallet = new Wallet(USER_PRIVATE_KEY, provider); + + console.log("Step 1: Checking FXRP balance and allowance..."); + const status = await checkUserStatus(provider, FORWARDER_ADDRESS, wallet.address); + console.log(` Balance: ${status.balanceFormatted}, Allowance: ${status.allowanceFormatted}, Nonce: ${status.nonce}`); + + if (status.needsApproval) { + console.log("\nStep 2: Approving FXRP..."); + const approval = await approveFXRP(wallet, FORWARDER_ADDRESS); + console.log(` TX: ${approval.transactionHash}`); + } else { + console.log("\nStep 2: Already approved."); + } + + const recipient = process.env.RECIPIENT_ADDRESS ?? "0x0000000000000000000000000000000000000001"; + const amountFXRP = "0.1"; + const feeFXRP = "0.01"; + + console.log("\nStep 3: Creating payment request..."); + const paymentRequest = await createPaymentRequest( + wallet, + FORWARDER_ADDRESS, + recipient, + amountFXRP, + feeFXRP + ); + console.log(` Amount: ${paymentRequest.meta.amountFormatted}, Fee: ${paymentRequest.meta.feeFormatted}`); + + console.log("\nStep 4: Submitting to relayer..."); + try { + const response = await fetch(`${RELAYER_URL}/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(paymentRequest), + }); + const result = (await response.json()) as { success?: boolean; transactionHash?: string; error?: string }; + if (result.success) { + console.log(` Success. TX: ${result.transactionHash}`); + } else { + console.log(` Failed: ${result.error}`); + } + } catch (error) { + console.log(` Error: ${(error as Error).message}. Is the relayer running at ${RELAYER_URL}?`); + } +} + +main().catch(console.error); diff --git a/examples/developer-hub-javascript/fxrp-gasless/payment.ts b/examples/developer-hub-javascript/fxrp-gasless/payment.ts new file mode 100644 index 00000000..697e3f5b --- /dev/null +++ b/examples/developer-hub-javascript/fxrp-gasless/payment.ts @@ -0,0 +1,239 @@ +/** + * FXRP Gasless Payment Utilities + * + * Copy this file to utils/payment.ts in your project. + * Requires typechain-types (run "npx hardhat compile" after adding the contract). + */ + +import { ethers, Contract, Wallet, Provider } from "ethers"; +import { erc20Abi, type TypedDataDomain, type TypedData } from "viem"; +import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; + +const DEFAULT_DEADLINE_SECONDS = 30 * 60; + +export const EIP712_DOMAIN: TypedDataDomain = { + name: "GaslessPaymentForwarder", + version: "1", +}; + +export const PAYMENT_REQUEST_TYPES = { + PaymentRequest: [ + { name: "from", type: "address" as const }, + { name: "to", type: "address" as const }, + { name: "amount", type: "uint256" as const }, + { name: "fee", type: "uint256" as const }, + { name: "nonce", type: "uint256" as const }, + { name: "deadline", type: "uint256" as const }, + ], +} satisfies TypedData; + +export interface SignPaymentParams { + forwarderAddress: string; + to: string; + amount: bigint; + fee: bigint; + nonce: bigint; + deadline: number; + chainId: bigint; +} + +export interface PaymentRequest { + from: string; + to: string; + amount: string; + fee: string; + deadline: number; + signature: string; + meta: { + amountFormatted: string; + feeFormatted: string; + nonce: string; + chainId: string; + }; +} + +export interface ApprovalResult { + transactionHash: string; + blockNumber: number | null; + fxrpAddress: string; + approved: string; +} + +export interface UserStatus { + fxrpAddress: string; + balance: string; + balanceFormatted: string; + allowance: string; + allowanceFormatted: string; + nonce: string; + needsApproval: boolean; +} + +export function parseAmount(amount: string | number, decimals: number): bigint { + return ethers.parseUnits(amount.toString(), decimals); +} + +export function formatAmount(drops: bigint | string, decimals: number): string { + return ethers.formatUnits(drops, decimals); +} + +export async function getTokenDecimals( + provider: Provider, + forwarderAddress: string +): Promise { + const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const fxrpAddress: string = await forwarder.fxrp(); + const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, provider); + return (await fxrp.decimals()) as number; +} + +export async function getNonce( + provider: Provider, + forwarderAddress: string, + userAddress: string +): Promise { + const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + return await forwarder.getNonce(userAddress); +} + +export async function getRelayerFee( + provider: Provider, + forwarderAddress: string +): Promise { + const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + return await forwarder.relayerFee(); +} + +export async function signPaymentRequest(wallet: Wallet, params: SignPaymentParams): Promise { + const { forwarderAddress, to, amount, fee, nonce, deadline, chainId } = params; + + const domain = { + ...EIP712_DOMAIN, + chainId: chainId, + verifyingContract: forwarderAddress, + }; + + const message = { + from: wallet.address, + to: to, + amount: amount, + fee: fee, + nonce: nonce, + deadline: deadline, + }; + + const signature = await wallet.signTypedData(domain, PAYMENT_REQUEST_TYPES, message); + return signature; +} + +export async function createPaymentRequest( + wallet: Wallet, + forwarderAddress: string, + to: string, + amount: string | number, + fee: string | number | null = null, + deadlineSeconds: number = DEFAULT_DEADLINE_SECONDS +): Promise { + const provider = wallet.provider; + if (!provider) { + throw new Error("Wallet must be connected to a provider"); + } + + const [network, decimals] = await Promise.all([ + provider.getNetwork(), + getTokenDecimals(provider, forwarderAddress), + ]); + const chainId = network.chainId; + + const nonce = await getNonce(provider, forwarderAddress, wallet.address); + + let feeDrops: bigint; + if (fee !== null) { + feeDrops = parseAmount(fee, decimals); + } else { + feeDrops = await getRelayerFee(provider, forwarderAddress); + } + + const block = await provider.getBlock("latest"); + const chainTime = block?.timestamp ?? Math.floor(Date.now() / 1000); + const deadline = chainTime + deadlineSeconds; + + const amountDrops = parseAmount(amount, decimals); + + const signature = await signPaymentRequest(wallet, { + forwarderAddress, + to, + amount: amountDrops, + fee: feeDrops, + nonce, + deadline, + chainId, + }); + + return { + from: wallet.address, + to: to, + amount: amountDrops.toString(), + fee: feeDrops.toString(), + deadline: deadline, + signature: signature, + meta: { + amountFormatted: formatAmount(amountDrops, decimals) + " FXRP", + feeFormatted: formatAmount(feeDrops, decimals) + " FXRP", + nonce: nonce.toString(), + chainId: chainId.toString(), + }, + }; +} + +export async function approveFXRP( + wallet: Wallet, + forwarderAddress: string, + amount: bigint = ethers.MaxUint256 +): Promise { + const provider = wallet.provider; + if (!provider) { + throw new Error("Wallet must be connected to a provider"); + } + + const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const fxrpAddress: string = await forwarder.fxrp(); + + const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, wallet); + const tx = await fxrp.approve(forwarderAddress, amount); + const receipt = await tx.wait(); + + return { + transactionHash: tx.hash, + blockNumber: receipt?.blockNumber ?? null, + fxrpAddress: fxrpAddress, + approved: amount.toString(), + }; +} + +export async function checkUserStatus( + provider: Provider, + forwarderAddress: string, + userAddress: string +): Promise { + const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const fxrpAddress: string = await forwarder.fxrp(); + const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, provider); + + const [balance, allowance, nonce, decimals] = await Promise.all([ + fxrp.balanceOf(userAddress) as Promise, + fxrp.allowance(userAddress, forwarderAddress) as Promise, + forwarder.getNonce(userAddress) as Promise, + fxrp.decimals() as Promise, + ]); + + return { + fxrpAddress, + balance: balance.toString(), + balanceFormatted: formatAmount(balance, decimals) + " FXRP", + allowance: allowance.toString(), + allowanceFormatted: formatAmount(allowance, decimals) + " FXRP", + nonce: nonce.toString(), + needsApproval: allowance === 0n, + }; +} diff --git a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts new file mode 100644 index 00000000..dc3ef5be --- /dev/null +++ b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts @@ -0,0 +1,257 @@ +/** + * FXRP Gasless Payment Relayer Service + * + * Copy this file to relayer/index.ts in your project. + * Requires typechain-types (run "npx hardhat compile" after adding the contract). + * + * Environment: RELAYER_PRIVATE_KEY, FORWARDER_ADDRESS, RPC_URL (optional), PORT (optional). + * Run: npx ts-node relayer/index.ts + */ + +import { ethers, Contract, Wallet, JsonRpcProvider } from "ethers"; +import { erc20Abi, recoverTypedDataAddress, type TypedDataDomain, type TypedData } from "viem"; +import express, { Request, Response } from "express"; +import cors from "cors"; +import "dotenv/config"; +import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; +import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; + +const EIP712_DOMAIN: TypedDataDomain = { + name: "GaslessPaymentForwarder", + version: "1", +}; + +const PAYMENT_REQUEST_TYPES = { + PaymentRequest: [ + { name: "from", type: "address" as const }, + { name: "to", type: "address" as const }, + { name: "amount", type: "uint256" as const }, + { name: "fee", type: "uint256" as const }, + { name: "nonce", type: "uint256" as const }, + { name: "deadline", type: "uint256" as const }, + ], +} satisfies TypedData; + +const NETWORKS: Record = { + flare: { rpc: "https://flare-api.flare.network/ext/C/rpc", chainId: 14 }, + coston2: { rpc: "https://coston2-api.flare.network/ext/C/rpc", chainId: 114 }, + songbird: { rpc: "https://songbird-api.flare.network/ext/C/rpc", chainId: 19 }, +}; + +export interface RelayerConfig { + relayerPrivateKey: string; + forwarderAddress: string; + rpcUrl?: string; +} + +export interface PaymentRequest { + from: string; + to: string; + amount: string; + fee: string; + deadline: number; + signature: string; +} + +export interface ExecuteResult { + success: boolean; + transactionHash: string; + blockNumber: number | null; + gasUsed: string; +} + +export class GaslessRelayer { + private config: RelayerConfig; + private provider: JsonRpcProvider; + private wallet: Wallet; + private forwarder: GaslessPaymentForwarder; + + constructor(config: RelayerConfig) { + this.config = config; + const rpcUrl = config.rpcUrl || NETWORKS.coston2.rpc; + this.provider = new JsonRpcProvider(rpcUrl); + this.wallet = new Wallet(config.relayerPrivateKey, this.provider); + this.forwarder = GaslessPaymentForwarder__factory.connect( + config.forwarderAddress, + this.wallet + ); + } + + async executePayment(request: PaymentRequest): Promise { + const from = ethers.getAddress(request.from); + const to = ethers.getAddress(request.to); + const amount = BigInt(request.amount); + const fee = BigInt(request.fee); + const deadline = Number(request.deadline); + const sig = request.signature; + if (typeof sig !== "string" || sig.length < 130) { + throw new Error("Invalid signature: must be a hex string"); + } + const signature = sig.startsWith("0x") ? sig : "0x" + sig; + + const chainId = (await this.provider.getNetwork()).chainId; + const nonce = await this.forwarder.getNonce(from); + const domain: TypedDataDomain = { + ...EIP712_DOMAIN, + chainId: Number(chainId), + verifyingContract: ethers.getAddress(this.config.forwarderAddress) as `0x${string}`, + }; + const message = { from, to, amount, fee, nonce, deadline }; + + let recoveredAddress: string; + try { + recoveredAddress = await recoverTypedDataAddress({ + domain, + types: PAYMENT_REQUEST_TYPES, + primaryType: "PaymentRequest", + message, + signature: signature as `0x${string}`, + }); + } catch (e) { + throw new Error( + `Invalid signature format: ${e instanceof Error ? e.message : String(e)}` + ); + } + if (recoveredAddress.toLowerCase() !== from.toLowerCase()) { + throw new Error( + `Signature invalid: recovered ${recoveredAddress} but expected ${from}. Check chainId, forwarder address, and nonce.` + ); + } + + await this.validateRequest({ from, to, amount: amount.toString(), fee: fee.toString(), deadline, signature }); + + try { + await this.forwarder.executePayment.staticCall(from, to, amount, fee, deadline, signature); + } catch (simError) { + const err = simError as Error & { reason?: string }; + throw new Error(`Contract simulation failed: ${err.reason || err.message}`); + } + + const nonceNow = await this.forwarder.getNonce(from); + if (nonceNow !== nonce) { + throw new Error("Nonce changed. Create a new payment request."); + } + + let gasLimit: bigint; + try { + const estimated = await this.forwarder.executePayment.estimateGas( + from, to, amount, fee, deadline, signature + ); + gasLimit = (estimated * 130n) / 100n; + } catch (estError) { + throw new Error(`Gas estimation failed: ${(estError as Error).message}`); + } + + const tx = await this.forwarder.executePayment( + from, to, amount, fee, deadline, signature, + { gasLimit } + ); + const receipt = await tx.wait(); + + return { + success: true, + transactionHash: tx.hash, + blockNumber: receipt?.blockNumber ?? null, + gasUsed: receipt?.gasUsed?.toString() ?? "0", + }; + } + + private async validateRequest(request: PaymentRequest): Promise { + const { from, amount, fee, deadline } = request; + const block = await this.provider.getBlock("latest"); + const chainTime = block?.timestamp ?? Math.floor(Date.now() / 1000); + if (deadline <= chainTime) { + throw new Error("Payment request has expired"); + } + const fxrpAddress: string = await this.forwarder.fxrp(); + const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, this.provider); + const decimals = (await fxrp.decimals()) as number; + const balance: bigint = await fxrp.balanceOf(from); + const totalRequired = BigInt(amount) + BigInt(fee); + if (balance < totalRequired) { + throw new Error( + `Insufficient FXRP balance. Required: ${ethers.formatUnits(totalRequired, decimals)}` + ); + } + const allowance: bigint = await fxrp.allowance(from, this.config.forwarderAddress); + if (allowance < totalRequired) { + throw new Error("Insufficient FXRP allowance. User must approve the forwarder."); + } + const minFee: bigint = await this.forwarder.relayerFee(); + if (BigInt(fee) < minFee) { + throw new Error(`Fee too low. Minimum: ${ethers.formatUnits(minFee, decimals)} FXRP`); + } + } + + async getNonce(address: string): Promise { + return await this.forwarder.getNonce(address); + } + + async getRelayerFee(): Promise { + return await this.forwarder.relayerFee(); + } + + async getTokenDecimals(): Promise { + const fxrpAddress: string = await this.forwarder.fxrp(); + const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, this.provider); + return (await fxrp.decimals()) as number; + } +} + +async function main(): Promise { + const relayerPrivateKey = process.env.RELAYER_PRIVATE_KEY; + const forwarderAddress = process.env.FORWARDER_ADDRESS; + const rpcUrl = process.env.RPC_URL; + const port = parseInt(process.env.PORT || "3000", 10); + + if (!relayerPrivateKey || !forwarderAddress) { + console.error("RELAYER_PRIVATE_KEY and FORWARDER_ADDRESS are required"); + process.exit(1); + } + + const relayer = new GaslessRelayer({ relayerPrivateKey, forwarderAddress, rpcUrl }); + const app = express(); + app.use(cors()); + app.use(express.json()); + + app.get("/nonce/:addr", async (req: Request<{ addr: string }>, res: Response) => { + try { + const nonce = await relayer.getNonce(req.params.addr); + res.json({ nonce: nonce.toString() }); + } catch (error) { + res.status(400).json({ error: (error as Error).message }); + } + }); + + app.get("/fee", async (_req: Request, res: Response) => { + try { + const [fee, decimals] = await Promise.all([ + relayer.getRelayerFee(), + relayer.getTokenDecimals(), + ]); + res.json({ + fee: fee.toString(), + feeFormatted: ethers.formatUnits(fee, decimals) + " FXRP", + }); + } catch (error) { + res.status(400).json({ error: (error as Error).message }); + } + }); + + app.post("/execute", async (req: Request, res: Response) => { + try { + const result = await relayer.executePayment(req.body); + res.json(result); + } catch (error) { + console.error("Payment execution failed:", (error as Error).message); + res.status(400).json({ error: (error as Error).message }); + } + }); + + app.listen(port, () => { + console.log(`Relayer running on http://localhost:${port}`); + console.log(" GET /nonce/:addr GET /fee POST /execute"); + }); +} + +main().catch(console.error); diff --git a/sidebars.ts b/sidebars.ts index 2896ae01..475d921a 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -282,6 +282,7 @@ const sidebars: SidebarsConfig = { "fxrp/token-interactions/fxrp-address", "fxrp/token-interactions/usdt0-fxrp-swap", "fxrp/token-interactions/x402-payments", + "fxrp/token-interactions/gasless-fxrp-payments", ], }, { From 992440709c304e59f899749eb3ef8d59196a3294 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Tue, 17 Feb 2026 09:49:20 +0200 Subject: [PATCH 02/12] feat(docs): enhance gasless FXRP payments guide with detailed examples and improved clarity --- .../04-gasless-fxrp-payments.mdx | 170 ++++++---- .../fxrp-gasless/deploy.ts | 59 +++- .../fxrp-gasless/example-usage.ts | 128 ++++++-- .../fxrp-gasless/payment.ts | 103 ++++-- .../fxrp-gasless/relayer.ts | 294 ++++++++++++++---- 5 files changed, 587 insertions(+), 167 deletions(-) diff --git a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx index 42cf6b0b..8795be8f 100644 --- a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx +++ b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx @@ -3,7 +3,8 @@ title: Gasless FXRP Payments tags: [intermediate, fassets, fxrp, meta-transactions] slug: gasless-fxrp-payments description: Transfer FXRP without gas using EIP-712 signed meta-transactions and a relayer. -keywords: [fassets, fxrp, gasless, meta-transactions, eip-712, flare-network, relayer] +keywords: + [fassets, fxrp, gasless, meta-transactions, eip-712, flare-network, relayer] sidebar_position: 4 --- @@ -14,13 +15,15 @@ import Relayer from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasles import DeployScript from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/deploy.ts"; import ExampleUsage from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts"; -This guide describes how to create a system for gasless FXRP (FAsset) transfers on Flare. -Users sign payment requests off-chain with [EIP-712](https://eips.ethereum.org/EIPS/eip-712), and a relayer submits them on-chain and pays gas on their behalf. +This guide explains how to set up gasless FXRP (FAsset) transfers on Flare. +Users sign payment requests off-chain with [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data, and a relayer submits them on-chain and pays gas on their behalf. -### EIP-3009 and EIP-712 +### Standards explained -[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) (Transfer with Authorization) extends [ERC-20](https://eips.ethereum.org/EIPS/eip-20) with meta-transactions. The token holder signs an authorization off-chain, and a relayer executes the transfer on-chain and pays gas. -This guide uses a **custom [EIP-712](https://eips.ethereum.org/EIPS/eip-712) payment message** and a forwarder contract instead of EIP-3009 on the token itself. +[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) (Transfer with Authorization) extends [ERC-20](https://eips.ethereum.org/EIPS/eip-20) with meta-transactions. +The token holder signs an authorization off-chain, and a relayer executes the transfer on-chain and pays gas. + +This guide uses a custom [EIP-712](https://eips.ethereum.org/EIPS/eip-712) payment message and a forwarder contract instead of EIP-3009 on the token itself. The forwarder pulls FXRP from the user (after a one-time approval) and sends it to the recipient. The relayer pays gas and can charge a fee in FXRP. @@ -39,33 +42,34 @@ Add the following files to your project: ### Gasless Payment Forwarder Contract -Save as `contracts/GaslessPaymentForwarder.sol`: +Save as `contracts/GaslessPaymentForwarder.sol` to create the forwarder smart contract that will be used to execute gasless payments.
-View GaslessPaymentForwarder.sol +View `contracts/GaslessPaymentForwarder.sol` {GaslessPaymentForwarder} -
- -#### How the contract works +**How the contract works** 1. Define libraries and state. 2. Define events. -3. Define errors. -4. Implement the contract constructor that sets the EIP-712 domain, owner, and initial relayer fee. -5. Implement the `fxrp` function that returns the FXRP token from the Flare registry. -6. Define the `executePayment` function that executes a gasless payment. -7. Hash the request (type hash + from, to, amount, fee, nonce, deadline) and get the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) digest. -8. Recover the signer from the signature with ECDSA. -9. Require signer from the payment request. -10. Require allowance is sufficient for the total amount (amount + fee). -11. Transfer amount from sender to recipient. -12. Transfer fee from sender to relayer (msg.sender); emit PaymentExecuted. -13. Define the view functions to get the nonce, domain separator, and payment request hash. -14. Define the functions to set the relayer authorization and fee. +3. Defines errors. +4. Implements the contract constructor that sets the EIP-712 domain, owner, and initial relayer fee. +5. Implements the `fxrp` function that returns the FXRP token from the Flare registry. +6. Defines the `executePayment` function that executes a gasless payment. +7. Hashes the request (type hash + from, to, amount, fee, nonce, deadline) and gets the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) digest. +8. Recovers the signer from the signature with ECDSA. +9. Requires the signer of the payment request. +10. Requires a sufficient allowance for the total amount (amount + fee). +11. Transfers the amount from the sender to the recipient. +12. Transfers fee from sender to relayer (msg.sender); emits `PaymentExecuted`. +13. Defines the view functions to get the nonce, domain separator, and payment request hash. +14. Defines the function to set the relater authorization. +15. Define the function to set the relayer fee. + + Run the following command to compile the contract and generate the typechain-types: @@ -75,37 +79,72 @@ npx hardhat compile ### Utilities -Save as `utils/payment.ts`. +Save as `utils/payment.ts` to create the utility functions that will be used to create and sign payment requests. + +:::info -It uses the `typechain-types` generated by the previous command. +This utility uses the `typechain-types` generated by the previous command. + +:::
-View utils/payment.ts +View `utils/payment.ts` {PaymentUtils} +**Code Breakdown** + +1. Import the necessary libraries. +2. Define the needed constants and EIP-712 types. +3. Define types for sign params, payment request, approval result, and user status. +4. Parse human-readable amount to raw bigint with decimals. +5. Format the raw amount to a readable string with decimals. +6. Get FXRP decimals from the forwarder and token contract. +7. Get the current nonce for a user from the forwarder. +8. Get the minimum relayer fee from the forwarder. +9. Sign the payment message with EIP-712 and return the signature. +10. Create a full payment request: fetch nonce and fee, set deadline, sign, and return payload for the relayer. +11. Approve the forwarder to spend FXRP. +12. Check user balance, allowance, nonce, and whether approval is needed. +
### Relayer Service -This is a relayer service that will submit payment requests to the Flare blockchain and pay gas on behalf of the user. +This is a relayer service that submits payment requests to the Flare blockchain and pays gas on behalf of the user. -Save as `relayer/index.ts` to start the relayer service: +Save as `relayer/index.ts` to start the relayer service.
-View relayer/index.ts +View `relayer/index.ts` {Relayer} +**Code Breakdown** + +1. Import necessary libraries. +2. Define EIP-712 domain, payment request types, and other network configurations. +3. Define `RelayerConfig`, `PaymentRequest`, and `ExecuteResult` types. +4. Implement the `GaslessRelayer` class to interact with the forwarder contract. +5. Execute gasless payment: normalize and validate request, recover signer with EIP-712, validate request (balance, allowance, deadline), staticCall, then recheck nonce, estimate gas with buffer, call executePayment, wait for receipt, return result. +6. Validate request: check deadline against chain time, check sender FXRP balance and allowance, check fee meets minimum. +7. Get nonce for an address from the forwarder contract. +8. Get the minimum relayer fee from the forwarder contract. +9. Get FXRP token decimals from the forwarder and token contract. +10. Get the relayer FLR balance for gas. +11. Start Express server. +12. Main function to read environment variables, create a relayer, log FLR balance, and start the server. +13. Run the main function to start the relayer service. +
### Deploy Script -Save as `scripts/deploy.ts` to deploy the [`GaslessPaymentForwarder`](/fxrp/token-interactions/gasless-fxrp-payments#gasless-payment-forwarder-contract) contract: +Save as `scripts/deploy.ts` to deploy the [`GaslessPaymentForwarder`](/fxrp/token-interactions/gasless-fxrp-payments#gasless-payment-forwarder-contract) contract.
View scripts/deploy.ts @@ -114,11 +153,23 @@ Save as `scripts/deploy.ts` to deploy the [`GaslessPaymentForwarder`](/fxrp/toke {DeployScript} +**Code Breakdown** + +1. Import necessary libraries. +2. Define default relayer fee constant in FXRP base units. +3. Get deployer signer and balance; log address and FLR balance. +4. Deploy `GaslessPaymentForwarder` contract with the relayer fee (FXRP comes from the Flare Contract Registry) and wait for deployment, and get the contract address. +5. Get FXRP token address and decimals from the forwarder. + Format fee for display. + Log deployment summary (network, contract, FXRP, owner, fee). +6. Verify contract on block explorer. +7. Run main function. +
### Example Flow -Use `scripts/example-usage.ts` to run the example flow: +Use `scripts/example-usage.ts` to run the example flow.
View scripts/example-usage.ts @@ -127,28 +178,32 @@ Use `scripts/example-usage.ts` to run the example flow: {ExampleUsage} -
+**Code Breakdown** -The example flow tests the system and executes the following steps: +1. Import the necessary libraries. +2. Read configuration from environment. +3. Validate the configuration. +4. Create a provider and a wallet. +5. Check user FXRP balance and allowance. +6. Approve the forwarder to spend FXRP. +7. Create a payment request and sign it. +8. Submit the payment request to the relayer. +9. Call the main function to run the example flow. -1. Check the user's balance and allowance. -2. Approve the forwarder to spend FXRP if needed. -3. Create a payment request. -4. Submit the payment request to the relayer. -5. Check the payment status. + ## Configuration Create a `.env` file in your project root with the following variables: -| Variable | Description | -|----------|-------------| -| `PRIVATE_KEY` | Deployer private key (for deployment) | -| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas, receives FXRP fees) | -| `USER_PRIVATE_KEY` | User wallet for testing | -| `FORWARDER_ADDRESS` | Deployed contract address (set after deploy) | -| `RPC_URL` | Flare network RPC (e.g. Coston2 testnet) | -| `RELAYER_URL` | Relayer HTTP URL (e.g. `http://localhost:3000`) | +| Variable | Description | +| --------------------- | ----------------------------------------------- | +| `PRIVATE_KEY` | Deployer private key (for deployment) | +| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas, receives FXRP fees) | +| `USER_PRIVATE_KEY` | User wallet for testing | +| `FORWARDER_ADDRESS` | Deployed contract address (set after deploy) | +| `RPC_URL` | Flare network RPC (e.g. Coston2 testnet) | +| `RELAYER_URL` | Relayer HTTP URL (e.g. `http://localhost:3000`) | ## Deploy and run @@ -182,11 +237,11 @@ npx ts-node relayer/index.ts **Main relayer backend endpoints:** -| Method | Path | Description | -|--------|------|-------------| -| POST | `/execute` | Execute single payment | -| GET | `/nonce/:address` | Get nonce for address | -| GET | `/fee` | Get the relayer fee | +| Method | Path | Description | +| ------ | ----------------- | ---------------------- | +| POST | `/execute` | Execute single payment | +| GET | `/nonce/:address` | Get nonce for address | +| GET | `/fee` | Get the relayer fee | ### Run the example Flow @@ -198,11 +253,11 @@ npx ts-node scripts/example-usage.ts This script will execute the following steps: -1. Check the user's balance and allowance. -2. Approve the forwarder to spend FXRP if needed. -3. Create a payment request. -4. Submit the payment request to the relayer. -5. Check the payment status. +1. Checks the user's balance and allowance. +2. Approves the forwarder to spend FXRP if needed. +3. Creates a payment request. +4. Submits the payment request to the relayer. +5. Checks the payment status. You should see the following output: @@ -248,8 +303,9 @@ Step 4: Submitting to relayer... :::tip[Next steps] + - [Get FXRP token address](/fxrp/token-interactions/fxrp-address) for use in your app. - [Swap USDT0 to FXRP](/fxrp/token-interactions/usdt0-fxrp-swap) to acquire FXRP. - Review [FXRP operational parameters](/fxrp/parameters). - [Gasless USD₮0 Transfers](/network/guides/gasless-usdt0-transfers) — Similar pattern for USDT0 on Flare. -::: + ::: diff --git a/examples/developer-hub-javascript/fxrp-gasless/deploy.ts b/examples/developer-hub-javascript/fxrp-gasless/deploy.ts index 585d89f1..fb213335 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/deploy.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/deploy.ts @@ -1,48 +1,93 @@ /** - * Deploy GaslessPaymentForwarder + * Deploy the GaslessPaymentForwarder contract * - * Copy to scripts/deploy.ts. Usage: npx hardhat run scripts/deploy.ts --network coston2 + * Usage: npm run deploy:coston2 */ +// 1. Import the necessary libraries import hre from "hardhat"; import { erc20Abi } from "viem"; import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; import type { IERC20Metadata } from "../typechain-types/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata"; +// 2. Define the default relayer fee (FXRP token base units) const DEFAULT_RELAYER_FEE = 10000n; async function main(): Promise { const network = hre.network.name; console.log(`\nDeploying GaslessPaymentForwarder to ${network}...`); + // 3. Get deployer account and balance const [deployer] = await hre.ethers.getSigners(); console.log(`Deployer address: ${deployer.address}`); const balance = await hre.ethers.provider.getBalance(deployer.address); console.log(`Deployer balance: ${hre.ethers.formatEther(balance)} FLR`); - const GaslessPaymentForwarder = await hre.ethers.getContractFactory("GaslessPaymentForwarder"); - const forwarder = (await GaslessPaymentForwarder.deploy(DEFAULT_RELAYER_FEE)) as unknown as GaslessPaymentForwarder; + console.log(`\nFXRP address will be fetched from Flare Contract Registry`); + console.log( + ` ContractRegistry.getAssetManagerFXRP() -> AssetManager.fAsset()`, + ); + console.log(`Relayer fee: ${DEFAULT_RELAYER_FEE} (base units)`); + + // 4. Deploy the GaslessPaymentForwarder contract (FXRP fetched from registry) + const GaslessPaymentForwarder = await hre.ethers.getContractFactory( + "GaslessPaymentForwarder", + ); + const forwarder = (await GaslessPaymentForwarder.deploy( + DEFAULT_RELAYER_FEE, + )) as unknown as GaslessPaymentForwarder; await forwarder.waitForDeployment(); const forwarderAddress = await forwarder.getAddress(); + // 5. Get FXRP token address from forwarder (for display) const fxrpAddress = await forwarder.fxrp(); const fxrp = new hre.ethers.Contract( fxrpAddress, erc20Abi as unknown as string[], - hre.ethers.provider + hre.ethers.provider, ) as unknown as IERC20Metadata; const decimals = await fxrp.decimals(); - const relayerFeeFormatted = hre.ethers.formatUnits(DEFAULT_RELAYER_FEE, decimals); + const relayerFeeFormatted = hre.ethers.formatUnits( + DEFAULT_RELAYER_FEE, + decimals, + ); console.log(`\nGaslessPaymentForwarder deployed to: ${forwarderAddress}`); + console.log("\n--- Deployment Summary ---"); + console.log(`Network: ${network}`); + console.log(`Contract: ${forwarderAddress}`); console.log(`FXRP Token: ${fxrpAddress}`); - console.log(`Relayer Fee: ${relayerFeeFormatted} FXRP`); + console.log(`Owner: ${deployer.address}`); + console.log( + `Relayer Fee: ${relayerFeeFormatted} FXRP (${DEFAULT_RELAYER_FEE} base units)`, + ); + + // 6. Verify contract on block explorer + console.log("\nVerifying contract..."); + try { + await hre.run("verify:verify", { + address: forwarderAddress, + constructorArguments: [DEFAULT_RELAYER_FEE.toString()], + }); + console.log("Contract verified successfully."); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes("Already Verified") || msg.includes("already verified")) { + console.log("Contract already verified."); + } else { + console.warn("Verification failed:", msg); + console.log( + `\nTo verify manually: npx hardhat verify --network ${network} ${forwarderAddress} "${DEFAULT_RELAYER_FEE}"`, + ); + } + } return forwarderAddress; } +// 7. Main entry point for the deployment script main() .then(() => process.exit(0)) .catch((error) => { diff --git a/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts b/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts index aa141cb5..47af79bf 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts @@ -1,70 +1,148 @@ /** - * Example: full FXRP gasless payment flow + * Example: How to use the FXRP Gasless Payment System * - * Copy to scripts/example-usage.ts. Usage: npx ts-node scripts/example-usage.ts - * Prerequisites: Deploy forwarder, set FORWARDER_ADDRESS, start relayer, have FXRP. + * This script demonstrates the complete flow: + * 1. Check user's FXRP balance and allowance + * 2. Approve the forwarder (if needed) + * 3. Create and sign a gasless payment request + * 4. Submit to the relayer + * + * Prerequisites: + * - Deploy the GaslessPaymentForwarder contract + * - Set the FORWARDER_ADDRESS in the environment variables + * - Start the relayer service + * - Have FXRP in your wallet + * + * Usage: npm run example */ +// 1. Import the necessary libraries import { Wallet, JsonRpcProvider } from "ethers"; -import { createPaymentRequest, approveFXRP, checkUserStatus } from "../utils/payment"; +import { + createPaymentRequest, + approveFXRP, + checkUserStatus, +} from "../utils/payment"; import "dotenv/config"; +// 2. Configuration from environment const RPC_URL = process.env.RPC_URL; -const RELAYER_URL = process.env.RELAYER_URL ?? "http://localhost:3000"; +const RELAYER_URL = process.env.RELAYER_URL; const FORWARDER_ADDRESS = process.env.FORWARDER_ADDRESS; const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY; +// Relayer /execute API response type +interface RelayerResponse { + success?: boolean; + transactionHash?: string; + blockNumber?: number; + gasUsed?: string; + error?: string; +} + async function main(): Promise { - if (!FORWARDER_ADDRESS || !USER_PRIVATE_KEY) { - console.error("FORWARDER_ADDRESS and USER_PRIVATE_KEY required"); + console.log("=== FXRP Gasless Payment Example ===\n"); + + // 3. Validate configuration + if (!FORWARDER_ADDRESS) { + console.error("Error: FORWARDER_ADDRESS not set in environment"); + process.exit(1); + } + if (!USER_PRIVATE_KEY) { + console.error("Error: USER_PRIVATE_KEY not set in environment"); process.exit(1); } + // 4. Setup provider and wallet const provider = new JsonRpcProvider(RPC_URL); const wallet = new Wallet(USER_PRIVATE_KEY, provider); + console.log(`User address: ${wallet.address}`); + console.log(`Forwarder: ${FORWARDER_ADDRESS}`); + console.log(`Relayer: ${RELAYER_URL}\n`); + + // 5. Check user FXRP balance and allowance console.log("Step 1: Checking FXRP balance and allowance..."); - const status = await checkUserStatus(provider, FORWARDER_ADDRESS, wallet.address); - console.log(` Balance: ${status.balanceFormatted}, Allowance: ${status.allowanceFormatted}, Nonce: ${status.nonce}`); + const status = await checkUserStatus( + provider, + FORWARDER_ADDRESS, + wallet.address, + ); + + console.log(` FXRP Token: ${status.fxrpAddress}`); + console.log(` Balance: ${status.balanceFormatted}`); + console.log(` Allowance: ${status.allowanceFormatted}`); + console.log(` Nonce: ${status.nonce}`); + // 6. Approve FXRP for forwarder (if needed) if (status.needsApproval) { - console.log("\nStep 2: Approving FXRP..."); - const approval = await approveFXRP(wallet, FORWARDER_ADDRESS); - console.log(` TX: ${approval.transactionHash}`); + console.log("\nStep 2: Approving FXRP for gasless payments..."); + const approvalResult = await approveFXRP(wallet, FORWARDER_ADDRESS); + console.log(` Approved! TX: ${approvalResult.transactionHash}`); } else { - console.log("\nStep 2: Already approved."); + console.log("\nStep 2: Already approved, skipping..."); } - const recipient = process.env.RECIPIENT_ADDRESS ?? "0x0000000000000000000000000000000000000001"; - const amountFXRP = "0.1"; - const feeFXRP = "0.01"; + // 7. Create and sign the payment request + const recipientAddress = + process.env.RECIPIENT_ADDRESS || + "0x0000000000000000000000000000000000000001"; + const amountFXRP = "0.1"; // 0.1 FXRP + const feeFXRP = "0.01"; // 0.01 FXRP relayer fee + + console.log(`\nStep 3: Creating payment request...`); + console.log(` To: ${recipientAddress}`); + console.log(` Amount: ${amountFXRP} FXRP`); + console.log(` Fee: ${feeFXRP} FXRP`); - console.log("\nStep 3: Creating payment request..."); const paymentRequest = await createPaymentRequest( wallet, FORWARDER_ADDRESS, - recipient, + recipientAddress, amountFXRP, - feeFXRP + feeFXRP, + ); + + console.log(`\n Signed request created:`); + console.log(` From: ${paymentRequest.from}`); + console.log(` To: ${paymentRequest.to}`); + console.log(` Amount: ${paymentRequest.meta.amountFormatted}`); + console.log(` Fee: ${paymentRequest.meta.feeFormatted}`); + console.log( + ` Deadline: ${new Date(paymentRequest.deadline * 1000).toISOString()}`, ); - console.log(` Amount: ${paymentRequest.meta.amountFormatted}, Fee: ${paymentRequest.meta.feeFormatted}`); + console.log(` Signature: ${paymentRequest.signature.slice(0, 20)}...`); + + // 8. Submit the payment request to the relayer + console.log(`\nStep 4: Submitting to relayer...`); - console.log("\nStep 4: Submitting to relayer..."); try { const response = await fetch(`${RELAYER_URL}/execute`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(paymentRequest), }); - const result = (await response.json()) as { success?: boolean; transactionHash?: string; error?: string }; + + const result = (await response.json()) as RelayerResponse; + if (result.success) { - console.log(` Success. TX: ${result.transactionHash}`); + console.log(` Payment executed successfully!`); + console.log(` Transaction: ${result.transactionHash}`); + console.log(` Block: ${result.blockNumber}`); + console.log(` Gas used: ${result.gasUsed}`); } else { - console.log(` Failed: ${result.error}`); + console.log(` Payment failed: ${result.error}`); } } catch (error) { - console.log(` Error: ${(error as Error).message}. Is the relayer running at ${RELAYER_URL}?`); + const err = error as Error; + console.log(` Failed to reach relayer: ${err.message}`); + console.log(`\n Make sure the relayer is running at ${RELAYER_URL}`); + + // Output the request for manual submission + console.log(`\n You can manually submit this request:`); + console.log(JSON.stringify(paymentRequest, null, 2)); } } +// 9. Main entry point for the example usage script main().catch(console.error); diff --git a/examples/developer-hub-javascript/fxrp-gasless/payment.ts b/examples/developer-hub-javascript/fxrp-gasless/payment.ts index 697e3f5b..a3887627 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/payment.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/payment.ts @@ -1,21 +1,28 @@ /** * FXRP Gasless Payment Utilities * - * Copy this file to utils/payment.ts in your project. - * Requires typechain-types (run "npx hardhat compile" after adding the contract). + * This module provides utilities for users to sign gasless payment requests + * using EIP-712 typed data signatures. + * */ +// 1. Import the necessary libraries import { ethers, Contract, Wallet, Provider } from "ethers"; import { erc20Abi, type TypedDataDomain, type TypedData } from "viem"; import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; +// 2. Define the default deadline, EIP-712 domain and types + +// Default deadline: 30 minutes from now const DEFAULT_DEADLINE_SECONDS = 30 * 60; +// EIP-712 domain (viem TypedDataDomain format) export const EIP712_DOMAIN: TypedDataDomain = { name: "GaslessPaymentForwarder", version: "1", }; +// EIP-712 types (viem TypedData format - compatible with ethers signTypedData) export const PAYMENT_REQUEST_TYPES = { PaymentRequest: [ { name: "from", type: "address" as const }, @@ -27,6 +34,7 @@ export const PAYMENT_REQUEST_TYPES = { ], } satisfies TypedData; +// Type definitions export interface SignPaymentParams { forwarderAddress: string; to: string; @@ -69,50 +77,75 @@ export interface UserStatus { needsApproval: boolean; } +// 3. Parse amount from human-readable string using token decimals export function parseAmount(amount: string | number, decimals: number): bigint { return ethers.parseUnits(amount.toString(), decimals); } +// 4. Format amount from raw units to human-readable string export function formatAmount(drops: bigint | string, decimals: number): string { return ethers.formatUnits(drops, decimals); } +// 5. Get FXRP token decimals from the forwarder contract using the provider export async function getTokenDecimals( provider: Provider, - forwarderAddress: string + forwarderAddress: string, ): Promise { - const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const forwarder = GaslessPaymentForwarder__factory.connect( + forwarderAddress, + provider, + ); const fxrpAddress: string = await forwarder.fxrp(); - const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, provider); + const fxrp = new Contract( + fxrpAddress, + erc20Abi as ethers.InterfaceAbi, + provider, + ); return (await fxrp.decimals()) as number; } +// 6. Get the current nonce for a user from the forwarder contract export async function getNonce( provider: Provider, forwarderAddress: string, - userAddress: string + userAddress: string, ): Promise { - const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const forwarder = GaslessPaymentForwarder__factory.connect( + forwarderAddress, + provider, + ); return await forwarder.getNonce(userAddress); } +// 7. Get the minimum relayer fee from the forwarder contract export async function getRelayerFee( provider: Provider, - forwarderAddress: string + forwarderAddress: string, ): Promise { - const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const forwarder = GaslessPaymentForwarder__factory.connect( + forwarderAddress, + provider, + ); return await forwarder.relayerFee(); } -export async function signPaymentRequest(wallet: Wallet, params: SignPaymentParams): Promise { - const { forwarderAddress, to, amount, fee, nonce, deadline, chainId } = params; +// 8. Sign a payment request using EIP-712 +export async function signPaymentRequest( + wallet: Wallet, + params: SignPaymentParams, +): Promise { + const { forwarderAddress, to, amount, fee, nonce, deadline, chainId } = + params; + // Build the EIP-712 domain const domain = { ...EIP712_DOMAIN, chainId: chainId, verifyingContract: forwarderAddress, }; + // Build the message const message = { from: wallet.address, to: to, @@ -122,31 +155,41 @@ export async function signPaymentRequest(wallet: Wallet, params: SignPaymentPara deadline: deadline, }; - const signature = await wallet.signTypedData(domain, PAYMENT_REQUEST_TYPES, message); + // Sign the typed data + const signature = await wallet.signTypedData( + domain, + PAYMENT_REQUEST_TYPES, + message, + ); + return signature; } +// 9. Create a complete payment request ready for submission to a relayer export async function createPaymentRequest( wallet: Wallet, forwarderAddress: string, to: string, amount: string | number, fee: string | number | null = null, - deadlineSeconds: number = DEFAULT_DEADLINE_SECONDS + deadlineSeconds: number = DEFAULT_DEADLINE_SECONDS, ): Promise { const provider = wallet.provider; if (!provider) { throw new Error("Wallet must be connected to a provider"); } + // Get chain ID and token decimals const [network, decimals] = await Promise.all([ provider.getNetwork(), getTokenDecimals(provider, forwarderAddress), ]); const chainId = network.chainId; + // Get current nonce const nonce = await getNonce(provider, forwarderAddress, wallet.address); + // Get fee (use provided or contract default) let feeDrops: bigint; if (fee !== null) { feeDrops = parseAmount(fee, decimals); @@ -154,12 +197,15 @@ export async function createPaymentRequest( feeDrops = await getRelayerFee(provider, forwarderAddress); } + // Use chain block timestamp for deadline (avoids clock skew vs contract's block.timestamp) const block = await provider.getBlock("latest"); const chainTime = block?.timestamp ?? Math.floor(Date.now() / 1000); const deadline = chainTime + deadlineSeconds; + // Parse amount const amountDrops = parseAmount(amount, decimals); + // Sign the request const signature = await signPaymentRequest(wallet, { forwarderAddress, to, @@ -177,6 +223,7 @@ export async function createPaymentRequest( fee: feeDrops.toString(), deadline: deadline, signature: signature, + // Metadata (not part of signature) meta: { amountFormatted: formatAmount(amountDrops, decimals) + " FXRP", feeFormatted: formatAmount(feeDrops, decimals) + " FXRP", @@ -186,20 +233,30 @@ export async function createPaymentRequest( }; } +// 10. Approve the forwarder contract to spend FXRP (one-time per user) export async function approveFXRP( wallet: Wallet, forwarderAddress: string, - amount: bigint = ethers.MaxUint256 + amount: bigint = ethers.MaxUint256, ): Promise { const provider = wallet.provider; if (!provider) { throw new Error("Wallet must be connected to a provider"); } - const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + // Get FXRP token address from forwarder + const forwarder = GaslessPaymentForwarder__factory.connect( + forwarderAddress, + provider, + ); const fxrpAddress: string = await forwarder.fxrp(); - const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, wallet); + // Approve + const fxrp = new Contract( + fxrpAddress, + erc20Abi as ethers.InterfaceAbi, + wallet, + ); const tx = await fxrp.approve(forwarderAddress, amount); const receipt = await tx.wait(); @@ -211,14 +268,22 @@ export async function approveFXRP( }; } +// 11. Check user's FXRP balance and allowance export async function checkUserStatus( provider: Provider, forwarderAddress: string, - userAddress: string + userAddress: string, ): Promise { - const forwarder = GaslessPaymentForwarder__factory.connect(forwarderAddress, provider); + const forwarder = GaslessPaymentForwarder__factory.connect( + forwarderAddress, + provider, + ); const fxrpAddress: string = await forwarder.fxrp(); - const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, provider); + const fxrp = new Contract( + fxrpAddress, + erc20Abi as ethers.InterfaceAbi, + provider, + ); const [balance, allowance, nonce, decimals] = await Promise.all([ fxrp.balanceOf(userAddress) as Promise, diff --git a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts index dc3ef5be..5ebf6879 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts @@ -1,21 +1,35 @@ /** * FXRP Gasless Payment Relayer Service * - * Copy this file to relayer/index.ts in your project. - * Requires typechain-types (run "npx hardhat compile" after adding the contract). + * This service accepts signed payment requests from users and submits them + * to the blockchain, paying gas fees on behalf of users. * - * Environment: RELAYER_PRIVATE_KEY, FORWARDER_ADDRESS, RPC_URL (optional), PORT (optional). - * Run: npx ts-node relayer/index.ts + * Usage: + * npx ts-node relayer/index.ts + * + * Environment variables required: + * RELAYER_PRIVATE_KEY - Private key of the relayer wallet + * FORWARDER_ADDRESS - Address of the deployed GaslessPaymentForwarder contract + * RPC_URL - Flare network RPC URL (optional, defaults to Coston2 testnet) */ +// 1. Import the necessary libraries import { ethers, Contract, Wallet, JsonRpcProvider } from "ethers"; -import { erc20Abi, recoverTypedDataAddress, type TypedDataDomain, type TypedData } from "viem"; +import { + erc20Abi, + recoverTypedDataAddress, + type TypedDataDomain, + type TypedData, +} from "viem"; import express, { Request, Response } from "express"; import cors from "cors"; import "dotenv/config"; import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; +// 2. Define the network configurations + +// EIP-712 domain and types (viem format, must match contract) const EIP712_DOMAIN: TypedDataDomain = { name: "GaslessPaymentForwarder", version: "1", @@ -32,12 +46,23 @@ const PAYMENT_REQUEST_TYPES = { ], } satisfies TypedData; +// Network configurations const NETWORKS: Record = { - flare: { rpc: "https://flare-api.flare.network/ext/C/rpc", chainId: 14 }, - coston2: { rpc: "https://coston2-api.flare.network/ext/C/rpc", chainId: 114 }, - songbird: { rpc: "https://songbird-api.flare.network/ext/C/rpc", chainId: 19 }, + flare: { + rpc: "https://flare-api.flare.network/ext/C/rpc", + chainId: 14, + }, + coston2: { + rpc: "https://coston2-api.flare.network/ext/C/rpc", + chainId: 114, + }, + songbird: { + rpc: "https://songbird-api.flare.network/ext/C/rpc", + chainId: 19, + }, }; +// 3. Define the type definitions export interface RelayerConfig { relayerPrivateKey: string; forwarderAddress: string; @@ -60,6 +85,7 @@ export interface ExecuteResult { gasUsed: string; } +// 4. Define the GaslessRelayer class export class GaslessRelayer { private config: RelayerConfig; private provider: JsonRpcProvider; @@ -68,16 +94,26 @@ export class GaslessRelayer { constructor(config: RelayerConfig) { this.config = config; + + // Setup provider and wallet const rpcUrl = config.rpcUrl || NETWORKS.coston2.rpc; this.provider = new JsonRpcProvider(rpcUrl); this.wallet = new Wallet(config.relayerPrivateKey, this.provider); + + // Setup contract (generated ABI from typechain-types) this.forwarder = GaslessPaymentForwarder__factory.connect( config.forwarderAddress, - this.wallet + this.wallet, ); + + console.log(`Relayer initialized`); + console.log(` Relayer address: ${this.wallet.address}`); + console.log(` Forwarder contract: ${config.forwarderAddress}`); } + // 5. Execute a single gasless payment using the forwarder contract async executePayment(request: PaymentRequest): Promise { + // Normalize and validate request format const from = ethers.getAddress(request.from); const to = ethers.getAddress(request.to); const amount = BigInt(request.amount); @@ -89,15 +125,33 @@ export class GaslessRelayer { } const signature = sig.startsWith("0x") ? sig : "0x" + sig; + const normalizedRequest: PaymentRequest = { + from, + to, + amount: amount.toString(), + fee: fee.toString(), + deadline, + signature, + }; + + // Verify EIP-712 signature off-chain (catches domain/nonce mismatches before submitting) const chainId = (await this.provider.getNetwork()).chainId; const nonce = await this.forwarder.getNonce(from); const domain: TypedDataDomain = { ...EIP712_DOMAIN, chainId: Number(chainId), - verifyingContract: ethers.getAddress(this.config.forwarderAddress) as `0x${string}`, + verifyingContract: ethers.getAddress( + this.config.forwarderAddress, + ) as `0x${string}`, + }; + const message = { + from, + to, + amount, + fee, + nonce, + deadline, }; - const message = { from, to, amount, fee, nonce, deadline }; - let recoveredAddress: string; try { recoveredAddress = await recoverTypedDataAddress({ @@ -109,44 +163,87 @@ export class GaslessRelayer { }); } catch (e) { throw new Error( - `Invalid signature format: ${e instanceof Error ? e.message : String(e)}` + `Invalid signature format: ${e instanceof Error ? e.message : String(e)}`, ); } if (recoveredAddress.toLowerCase() !== from.toLowerCase()) { throw new Error( - `Signature invalid: recovered ${recoveredAddress} but expected ${from}. Check chainId, forwarder address, and nonce.` + `Signature invalid: recovered ${recoveredAddress} but expected ${from}. ` + + `Check chainId (expected ${chainId}), forwarder address, and nonce (expected ${nonce}).`, ); } - await this.validateRequest({ from, to, amount: amount.toString(), fee: fee.toString(), deadline, signature }); + // Validate the request + await this.validateRequest(normalizedRequest); + // Simulate first (fails fast, may yield better revert reason) try { - await this.forwarder.executePayment.staticCall(from, to, amount, fee, deadline, signature); + await this.forwarder.executePayment.staticCall( + from, + to, + amount, + fee, + deadline, + signature, + ); } catch (simError) { - const err = simError as Error & { reason?: string }; - throw new Error(`Contract simulation failed: ${err.reason || err.message}`); + const err = simError as Error & { reason?: string; data?: string }; + const msg = + err.reason || (err.data ? `revert data: ${err.data}` : err.message); + throw new Error(`Contract simulation failed: ${msg}`); } + // Re-check nonce right before send (prevents race if another request executed first) const nonceNow = await this.forwarder.getNonce(from); if (nonceNow !== nonce) { - throw new Error("Nonce changed. Create a new payment request."); + throw new Error( + `Nonce changed (was ${nonce}, now ${nonceNow}). ` + + `Payment may have been submitted by another request. Please create a new payment request.`, + ); } + // Estimate gas (staticCall uses block limit, so we must estimate for real tx) let gasLimit: bigint; try { const estimated = await this.forwarder.executePayment.estimateGas( - from, to, amount, fee, deadline, signature + from, + to, + amount, + fee, + deadline, + signature, ); - gasLimit = (estimated * 130n) / 100n; + gasLimit = (estimated * 130n) / 100n; // 30% buffer } catch (estError) { - throw new Error(`Gas estimation failed: ${(estError as Error).message}`); + const err = estError as Error; + throw new Error( + `Gas estimation failed (contract would revert): ${err.message}`, + ); } - const tx = await this.forwarder.executePayment( - from, to, amount, fee, deadline, signature, - { gasLimit } - ); - const receipt = await tx.wait(); + // Execute the payment + let tx: ethers.ContractTransactionResponse; + try { + tx = await this.forwarder.executePayment( + from, + to, + amount, + fee, + deadline, + signature, + { gasLimit }, + ); + } catch (sendError) { + throw sendError; + } + + // Wait for confirmation + let receipt: ethers.TransactionReceipt | null; + try { + receipt = await tx.wait(); + } catch (waitError) { + throw waitError; + } return { success: true, @@ -156,73 +253,109 @@ export class GaslessRelayer { }; } - private async validateRequest(request: PaymentRequest): Promise { + // 6. Validate a payment request before submission + async validateRequest(request: PaymentRequest): Promise { const { from, amount, fee, deadline } = request; + + // Check deadline against chain time (not local clock - avoids skew) const block = await this.provider.getBlock("latest"); const chainTime = block?.timestamp ?? Math.floor(Date.now() / 1000); if (deadline <= chainTime) { - throw new Error("Payment request has expired"); + throw new Error( + `Payment request has expired (deadline: ${deadline}, chain: ${chainTime})`, + ); } + + // Get FXRP token from forwarder const fxrpAddress: string = await this.forwarder.fxrp(); - const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, this.provider); + const fxrp = new Contract( + fxrpAddress, + erc20Abi as ethers.InterfaceAbi, + this.provider, + ); const decimals = (await fxrp.decimals()) as number; + + // Check sender's FXRP balance const balance: bigint = await fxrp.balanceOf(from); const totalRequired = BigInt(amount) + BigInt(fee); if (balance < totalRequired) { throw new Error( - `Insufficient FXRP balance. Required: ${ethers.formatUnits(totalRequired, decimals)}` + `Insufficient FXRP balance. Required: ${ethers.formatUnits(totalRequired, decimals)}, Available: ${ethers.formatUnits(balance, decimals)}`, ); } - const allowance: bigint = await fxrp.allowance(from, this.config.forwarderAddress); + + // Check allowance + const allowance: bigint = await fxrp.allowance( + from, + this.config.forwarderAddress, + ); if (allowance < totalRequired) { - throw new Error("Insufficient FXRP allowance. User must approve the forwarder."); + throw new Error( + `Insufficient FXRP allowance. Required: ${ethers.formatUnits(totalRequired, decimals)}, Approved: ${ethers.formatUnits(allowance, decimals)}`, + ); } + + // Check minimum fee const minFee: bigint = await this.forwarder.relayerFee(); if (BigInt(fee) < minFee) { - throw new Error(`Fee too low. Minimum: ${ethers.formatUnits(minFee, decimals)} FXRP`); + throw new Error( + `Fee too low. Minimum: ${ethers.formatUnits(minFee, decimals)} FXRP`, + ); } } + // 7. Get the current nonce for an address async getNonce(address: string): Promise { return await this.forwarder.getNonce(address); } + // 8. Get the minimum relayer fee async getRelayerFee(): Promise { return await this.forwarder.relayerFee(); } + // 9. Get the FXRP token decimals async getTokenDecimals(): Promise { const fxrpAddress: string = await this.forwarder.fxrp(); - const fxrp = new Contract(fxrpAddress, erc20Abi as ethers.InterfaceAbi, this.provider); + const fxrp = new Contract( + fxrpAddress, + erc20Abi as ethers.InterfaceAbi, + this.provider, + ); return (await fxrp.decimals()) as number; } -} -async function main(): Promise { - const relayerPrivateKey = process.env.RELAYER_PRIVATE_KEY; - const forwarderAddress = process.env.FORWARDER_ADDRESS; - const rpcUrl = process.env.RPC_URL; - const port = parseInt(process.env.PORT || "3000", 10); - - if (!relayerPrivateKey || !forwarderAddress) { - console.error("RELAYER_PRIVATE_KEY and FORWARDER_ADDRESS are required"); - process.exit(1); + // 10. Check relayer's FLR balance for gas + async getRelayerBalance(): Promise { + const balance = await this.provider.getBalance(this.wallet.address); + return ethers.formatEther(balance); } +} - const relayer = new GaslessRelayer({ relayerPrivateKey, forwarderAddress, rpcUrl }); +// 11. Express server for receiving payment requests +async function startServer( + relayer: GaslessRelayer, + port: number = 3000, +): Promise { const app = express(); app.use(cors()); app.use(express.json()); - app.get("/nonce/:addr", async (req: Request<{ addr: string }>, res: Response) => { - try { - const nonce = await relayer.getNonce(req.params.addr); - res.json({ nonce: nonce.toString() }); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); + // Get nonce for an address + app.get( + "/nonce/:addr", + async (req: Request<{ addr: string }>, res: Response) => { + try { + const nonce = await relayer.getNonce(req.params.addr); + res.json({ nonce: nonce.toString() }); + } catch (error) { + const err = error as Error; + res.status(400).json({ error: err.message }); + } + }, + ); + // Get relayer fee app.get("/fee", async (_req: Request, res: Response) => { try { const [fee, decimals] = await Promise.all([ @@ -234,24 +367,67 @@ async function main(): Promise { feeFormatted: ethers.formatUnits(fee, decimals) + " FXRP", }); } catch (error) { - res.status(400).json({ error: (error as Error).message }); + const err = error as Error; + res.status(400).json({ error: err.message }); } }); + // Execute payment app.post("/execute", async (req: Request, res: Response) => { try { const result = await relayer.executePayment(req.body); res.json(result); } catch (error) { - console.error("Payment execution failed:", (error as Error).message); - res.status(400).json({ error: (error as Error).message }); + const err = error as Error; + console.error("Payment execution failed:", err.message); + res.status(400).json({ error: err.message }); } }); app.listen(port, () => { - console.log(`Relayer running on http://localhost:${port}`); - console.log(" GET /nonce/:addr GET /fee POST /execute"); + console.log(`\nRelayer server running on http://localhost:${port}`); + console.log(`\nEndpoints:`); + console.log(` GET /nonce/:addr - Get nonce for address`); + console.log(` GET /fee - Get relayer fee`); + console.log(` POST /execute - Execute single payment`); + }); +} + +// 12. Main entry point for the relayer server +async function main(): Promise { + const relayerPrivateKey = process.env.RELAYER_PRIVATE_KEY; + const forwarderAddress = process.env.FORWARDER_ADDRESS; + const rpcUrl = process.env.RPC_URL; + const port = parseInt(process.env.PORT || "3000", 10); + + if (!relayerPrivateKey) { + console.error("Error: RELAYER_PRIVATE_KEY environment variable required"); + process.exit(1); + } + + if (!forwarderAddress) { + console.error("Error: FORWARDER_ADDRESS environment variable required"); + process.exit(1); + } + + const relayer = new GaslessRelayer({ + relayerPrivateKey, + forwarderAddress, + rpcUrl, }); + + // Check relayer balance + const balance = await relayer.getRelayerBalance(); + console.log(`Relayer FLR balance: ${balance} FLR`); + + if (parseFloat(balance) < 0.1) { + console.warn( + "Warning: Low relayer balance. Please fund the relayer wallet.", + ); + } + + await startServer(relayer, port); } +// 13. Run the server main().catch(console.error); From a747e1817a8344296828080491ca3150320c7093 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Tue, 17 Feb 2026 10:37:59 +0200 Subject: [PATCH 03/12] fix(docs): update import paths and improve type imports in gasless FXRP payment examples --- .../04-gasless-fxrp-payments.mdx | 2 +- .../fxrp-gasless/payment.ts | 2 +- .../fxrp-gasless/relayer.ts | 32 ++-- .../GaslessPaymentForwarder.sol | 175 ++++++++++++++++++ 4 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 examples/developer-hub-solidity/GaslessPaymentForwarder.sol diff --git a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx index 8795be8f..f5b5616a 100644 --- a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx +++ b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx @@ -9,7 +9,7 @@ sidebar_position: 4 --- import CodeBlock from "@theme/CodeBlock"; -import GaslessPaymentForwarder from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/GaslessPaymentForwarder.sol"; +import GaslessPaymentForwarder from "!!raw-loader!/examples/developer-hub-solidity/GaslessPaymentForwarder.sol"; import PaymentUtils from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/payment.ts"; import Relayer from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/relayer.ts"; import DeployScript from "!!raw-loader!/examples/developer-hub-javascript/fxrp-gasless/deploy.ts"; diff --git a/examples/developer-hub-javascript/fxrp-gasless/payment.ts b/examples/developer-hub-javascript/fxrp-gasless/payment.ts index a3887627..1ef72706 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/payment.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/payment.ts @@ -7,7 +7,7 @@ */ // 1. Import the necessary libraries -import { ethers, Contract, Wallet, Provider } from "ethers"; +import { ethers, Contract, type Wallet, type Provider } from "ethers"; import { erc20Abi, type TypedDataDomain, type TypedData } from "viem"; import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; diff --git a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts index 5ebf6879..50d28824 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts @@ -21,7 +21,7 @@ import { type TypedDataDomain, type TypedData, } from "viem"; -import express, { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import cors from "cors"; import "dotenv/config"; import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; @@ -222,28 +222,18 @@ export class GaslessRelayer { } // Execute the payment - let tx: ethers.ContractTransactionResponse; - try { - tx = await this.forwarder.executePayment( - from, - to, - amount, - fee, - deadline, - signature, - { gasLimit }, - ); - } catch (sendError) { - throw sendError; - } + const tx = await this.forwarder.executePayment( + from, + to, + amount, + fee, + deadline, + signature, + { gasLimit }, + ); // Wait for confirmation - let receipt: ethers.TransactionReceipt | null; - try { - receipt = await tx.wait(); - } catch (waitError) { - throw waitError; - } + const receipt = await tx.wait(); return { success: true, diff --git a/examples/developer-hub-solidity/GaslessPaymentForwarder.sol b/examples/developer-hub-solidity/GaslessPaymentForwarder.sol new file mode 100644 index 00000000..4ad51f80 --- /dev/null +++ b/examples/developer-hub-solidity/GaslessPaymentForwarder.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +// OpenZeppelin: SafeERC20, ECDSA, EIP712, Ownable, ReentrancyGuard +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +// Flare Contract Registry and FAsset interface +import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/flare/ContractRegistry.sol"; +import {IAssetManager} from "@flarenetwork/flare-periphery-contracts/flare/IAssetManager.sol"; +import {IFAsset} from "@flarenetwork/flare-periphery-contracts/flare/IFAsset.sol"; + +/** + * @title GaslessPaymentForwarder + * @notice Enables gasless FXRP transfers using EIP-712 signed meta-transactions + * @dev Users sign payment requests off-chain, relayers submit them on-chain. + * FXRP from Flare Contract Registry: getAssetManagerFXRP() -> fAsset(). + * + * Flow: (1) User approves this contract to spend FXRP once. + * (2) User signs PaymentRequest off-chain. (3) Relayer calls executePayment(). + * (4) Contract verifies signature and executes transfer. + */ +contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { + // 1. Define the necessary libraries and contract variables + using SafeERC20 for IFAsset; + using ECDSA for bytes32; + + mapping(address => uint256) public nonces; // replay protection per sender + mapping(address => bool) public authorizedRelayers; // relayer allowlist + + uint256 public relayerFee; // default fee (owner-configurable) + + // EIP-712 type hash for PaymentRequest + bytes32 public constant PAYMENT_REQUEST_TYPEHASH = + keccak256( + "PaymentRequest(address from,address to,uint256 amount,uint256 fee,uint256 nonce,uint256 deadline)" + ); + + // 2. Contract events + event PaymentExecuted( + address indexed from, + address indexed to, + uint256 amount, + uint256 fee, + uint256 nonce + ); + event RelayerAuthorized(address indexed relayer, bool authorized); // relayer allowlist changed + event RelayerFeeUpdated(uint256 newFee); // default fee changed + + // 3. Custom errors + error InvalidSignature(); // signer != from + error ExpiredRequest(); // block.timestamp > deadline + error InvalidNonce(); // nonce mismatch (replay) + error UnauthorizedRelayer(); // caller not in allowlist + error InsufficientAllowance(); // user approval < amount + fee + error ZeroAddress(); // zero address passed + + // 4. Constructor that initializes the relayer fee + constructor( + uint256 _relayerFee + ) EIP712("GaslessPaymentForwarder", "1") Ownable(msg.sender) { + relayerFee = _relayerFee; // set initial default fee + } + + // 5. Returns FXRP token from Flare Contract Registry + function fxrp() public view returns (IFAsset) { + IAssetManager assetManager = ContractRegistry.getAssetManagerFXRP(); + return IFAsset(address(assetManager.fAsset())); // FXRP token from registry + } + + // 6. Execute a gasless payment + function executePayment( + address from, + address to, + uint256 amount, + uint256 fee, + uint256 deadline, + bytes calldata signature + ) external nonReentrant { + if (block.timestamp > deadline) revert ExpiredRequest(); // validate deadline + + uint256 currentNonce = nonces[from]; + + // 7. Hash the payment request + bytes32 structHash = keccak256( + abi.encode( + PAYMENT_REQUEST_TYPEHASH, + from, + to, + amount, + fee, + currentNonce, + deadline + ) + ); + + // 8. Recover the signer from the hash + bytes32 hash = _hashTypedDataV4(structHash); + address signer = hash.recover(signature); + + // 9. Check if the signer is the from address + if (signer != from) revert InvalidSignature(); + + nonces[from] = currentNonce + 1; // increment nonce (prevents replay) + + IFAsset _fxrp = fxrp(); + + // 10. Check if the allowance is sufficient + uint256 totalAmount = amount + fee; + if (_fxrp.allowance(from, address(this)) < totalAmount) { + revert InsufficientAllowance(); + } + + // 11. Transfer the amount to the recipient + _fxrp.safeTransferFrom(from, to, amount); + + // 12. Transfer the fee to the relayer + if (fee > 0) { + _fxrp.safeTransferFrom(from, msg.sender, fee); + } + + emit PaymentExecuted(from, to, amount, fee, currentNonce); // log success + } + + // 13. Views for off-chain signing / validation + function getNonce(address account) external view returns (uint256) { + return nonces[account]; // current nonce for off-chain signing + } + + // Get the EIP-712 domain separator + function getDomainSeparator() external view returns (bytes32) { + return _domainSeparatorV4(); // EIP-712 domain separator + } + + // Compute the hash of a payment request for signing + function getPaymentRequestHash( + address from, + address to, + uint256 amount, + uint256 fee, + uint256 nonce, + uint256 deadline + ) external view returns (bytes32) { + bytes32 structHash = keccak256( + abi.encode( + PAYMENT_REQUEST_TYPEHASH, + from, + to, + amount, + fee, + nonce, + deadline + ) + ); + return _hashTypedDataV4(structHash); // full EIP-712 typed-data hash + } + + // 15. Set relayer authorization status + function setRelayerAuthorization( + address relayer, + bool authorized + ) external onlyOwner { + authorizedRelayers[relayer] = authorized; // update allowlist + emit RelayerAuthorized(relayer, authorized); + } + + // 15. Update the default relayer fee + function setRelayerFee(uint256 _relayerFee) external onlyOwner { + relayerFee = _relayerFee; // update default fee + emit RelayerFeeUpdated(_relayerFee); + } +} From 0d957c599f07dfb2d1381ebef34c04c139c02550 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Tue, 17 Feb 2026 17:53:44 +0200 Subject: [PATCH 04/12] feat(docs): integrate Mermaid theme for enhanced diagram support in gasless FXRP payments guide --- .../04-gasless-fxrp-payments.mdx | 117 +- docusaurus.config.ts | 2 + .../fxrp-gasless/deploy.ts | 38 +- .../fxrp-gasless/example-usage.ts | 4 - .../fxrp-gasless/payment.ts | 42 +- .../fxrp-gasless/relayer.ts | 77 +- .../GaslessPaymentForwarder.sol | 42 +- package-lock.json | 1451 +++++++++++++++-- package.json | 1 + src/theme/Mermaid/index.tsx | 97 ++ src/theme/Mermaid/styles.module.css | 7 + 11 files changed, 1537 insertions(+), 341 deletions(-) create mode 100644 src/theme/Mermaid/index.tsx create mode 100644 src/theme/Mermaid/styles.module.css diff --git a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx index f5b5616a..8f620842 100644 --- a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx +++ b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx @@ -24,14 +24,34 @@ Users sign payment requests off-chain with [EIP-712](https://eips.ethereum.org/E The token holder signs an authorization off-chain, and a relayer executes the transfer on-chain and pays gas. This guide uses a custom [EIP-712](https://eips.ethereum.org/EIPS/eip-712) payment message and a forwarder contract instead of EIP-3009 on the token itself. -The forwarder pulls FXRP from the user (after a one-time approval) and sends it to the recipient. -The relayer pays gas and can charge a fee in FXRP. +The forwarder pulls FXRP from the user (after a one-time approval) and sends it to the provided recipient. +The relayer pays gas on behalf of the user. + +## Architecture + +The architecture of the gasless FXRP payments system is as follows: + +```mermaid +sequenceDiagram + participant User + participant Relayer + participant GaslessPaymentForwarder + participant FXRP + + User->>User: 1. Sign payment (EIP-712)
off-chain, no gas + User->>Relayer: 2. POST signed request + Relayer->>GaslessPaymentForwarder: 3. executePayment()
(pays gas) + GaslessPaymentForwarder->>FXRP: 4. FXRP transfer + FXRP-->>GaslessPaymentForwarder: 5. Transfer confirmed + GaslessPaymentForwarder-->>Relayer: 6. Execution complete + Relayer-->>User: 7. Payment confirmed +``` ## Project setup -Create a new [Hardhat](https://hardhat.org/) project or use the [Flare Hardhat Starter](/network/guides/hardhat-foundry-starter-kit) kit. +To follow this guide, create a new [Hardhat](https://hardhat.org/) project or use the [Flare Hardhat Starter](/network/guides/hardhat-foundry-starter-kit) kit. -Install needed dependencies: +You will need to install the following dependencies: ```bash npm install ethers viem express cors dotenv @@ -53,21 +73,18 @@ Save as `contracts/GaslessPaymentForwarder.sol` to create the forwarder smart co **How the contract works** -1. Define libraries and state. -2. Define events. -3. Defines errors. -4. Implements the contract constructor that sets the EIP-712 domain, owner, and initial relayer fee. -5. Implements the `fxrp` function that returns the FXRP token from the Flare registry. -6. Defines the `executePayment` function that executes a gasless payment. -7. Hashes the request (type hash + from, to, amount, fee, nonce, deadline) and gets the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) digest. -8. Recovers the signer from the signature with ECDSA. -9. Requires the signer of the payment request. -10. Requires a sufficient allowance for the total amount (amount + fee). -11. Transfers the amount from the sender to the recipient. -12. Transfers fee from sender to relayer (msg.sender); emits `PaymentExecuted`. -13. Defines the view functions to get the nonce, domain separator, and payment request hash. -14. Defines the function to set the relater authorization. -15. Define the function to set the relayer fee. +1. Define libraries and state (nonces, authorizedRelayers). +2. Define events (PaymentExecuted, RelayerAuthorized). +3. Define errors. +4. Implement a constructor that sets the EIP-712 domain and owner. +5. Implement `fxrp` to return the FXRP token from the Flare Contract Registry. +6. Define `executePayment(from, to, amount, deadline, signature)`. +7. Hash the request (type hash + from, to, amount, nonce, deadline) and get the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) digest. +8. Recover the signer from the signature. +9. Require signer to be the from address; increment nonce. +10. Require sufficient allowance; transfer amount from sender to recipient. +11. Define view functions: getNonce, getDomainSeparator, getPaymentRequestHash. +12. Define setRelayerAuthorization for the relayer allowlist. @@ -77,9 +94,11 @@ Run the following command to compile the contract and generate the typechain-typ npx hardhat compile ``` +It will generate the `typechain-types` directory with the contract interfaces and factories. + ### Utilities -Save as `utils/payment.ts` to create the utility functions that will be used to create and sign payment requests. +Save as `utils/payment.ts` to create the utility functions that will be used to create and sign payment requests off-chain. :::info @@ -97,17 +116,16 @@ This utility uses the `typechain-types` generated by the previous command. **Code Breakdown** 1. Import the necessary libraries. -2. Define the needed constants and EIP-712 types. +2. Define constants and EIP-712 types (PaymentRequest: from, to, amount, nonce, deadline). 3. Define types for sign params, payment request, approval result, and user status. 4. Parse human-readable amount to raw bigint with decimals. 5. Format the raw amount to a readable string with decimals. 6. Get FXRP decimals from the forwarder and token contract. 7. Get the current nonce for a user from the forwarder. -8. Get the minimum relayer fee from the forwarder. -9. Sign the payment message with EIP-712 and return the signature. -10. Create a full payment request: fetch nonce and fee, set deadline, sign, and return payload for the relayer. -11. Approve the forwarder to spend FXRP. -12. Check user balance, allowance, nonce, and whether approval is needed. +8. Sign the payment message with EIP-712 and return the signature. +9. Create a full payment request: fetch nonce, set deadline, sign, return payload for the relayer. +10. Approve the forwarder to spend FXRP (one-time). +11. Check user balance, allowance, nonce, and whether approval is needed. @@ -127,18 +145,17 @@ Save as `relayer/index.ts` to start the relayer service. **Code Breakdown** 1. Import necessary libraries. -2. Define EIP-712 domain, payment request types, and other network configurations. -3. Define `RelayerConfig`, `PaymentRequest`, and `ExecuteResult` types. -4. Implement the `GaslessRelayer` class to interact with the forwarder contract. -5. Execute gasless payment: normalize and validate request, recover signer with EIP-712, validate request (balance, allowance, deadline), staticCall, then recheck nonce, estimate gas with buffer, call executePayment, wait for receipt, return result. -6. Validate request: check deadline against chain time, check sender FXRP balance and allowance, check fee meets minimum. -7. Get nonce for an address from the forwarder contract. -8. Get the minimum relayer fee from the forwarder contract. -9. Get FXRP token decimals from the forwarder and token contract. -10. Get the relayer FLR balance for gas. -11. Start Express server. -12. Main function to read environment variables, create a relayer, log FLR balance, and start the server. -13. Run the main function to start the relayer service. +2. Define EIP-712 domain and payment request types (from, to, amount, nonce, deadline); define NETWORKS (flare, coston2, songbird). +3. Define RelayerConfig, PaymentRequest, and ExecuteResult types. +4. Implement GaslessRelayer class: constructor sets provider, wallet, and forwarder. +5. Execute payment: normalize and validate request, recover signer with EIP-712, validate request (balance, allowance, deadline), staticCall, recheck nonce, estimate gas, call executePayment, wait for receipt, return result. +6. Validate request: check deadline against chain time, check sender balance and allowance. +7. Get nonce for an address from the forwarder. +8. Get FXRP token decimals from the forwarder and token contract. +9. Get the relayer FLR balance for gas. +10. Start Express server: GET /nonce/:addr, POST /execute. +11. Main: read env (RELAYER_PRIVATE_KEY, FORWARDER_ADDRESS, RPC_URL, PORT), create relayer, log FLR balance, start server. +12. Run main to start the relayer service. @@ -155,15 +172,12 @@ Save as `scripts/deploy.ts` to deploy the [`GaslessPaymentForwarder`](/fxrp/toke **Code Breakdown** -1. Import necessary libraries. -2. Define default relayer fee constant in FXRP base units. -3. Get deployer signer and balance; log address and FLR balance. -4. Deploy `GaslessPaymentForwarder` contract with the relayer fee (FXRP comes from the Flare Contract Registry) and wait for deployment, and get the contract address. -5. Get FXRP token address and decimals from the forwarder. - Format fee for display. - Log deployment summary (network, contract, FXRP, owner, fee). -6. Verify contract on block explorer. -7. Run main function. +1. Import Hardhat and TypeChain types. +2. Get deployer signer and balance; log address and FLR balance. +3. Deploy GaslessPaymentForwarder (no constructor args; FXRP from Flare Contract Registry); wait for deployment and get address. +4. Get FXRP token address from the forwarder; log deployment summary (network, contract, FXRP, owner). +5. Verify contract on block explorer (constructor arguments empty). +6. Run main: exit 0 on success, exit 1 on error. @@ -181,8 +195,8 @@ Use `scripts/example-usage.ts` to run the example flow. **Code Breakdown** 1. Import the necessary libraries. -2. Read configuration from environment. -3. Validate the configuration. +2. Read configuration from the environment like the RPC URL, relayer URL, forwarder address, and user private key. +3. Validate the configuration to ensure that the forwarder address and user private key are set. 4. Create a provider and a wallet. 5. Check user FXRP balance and allowance. 6. Approve the forwarder to spend FXRP. @@ -199,7 +213,7 @@ Create a `.env` file in your project root with the following variables: | Variable | Description | | --------------------- | ----------------------------------------------- | | `PRIVATE_KEY` | Deployer private key (for deployment) | -| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas, receives FXRP fees) | +| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas) | | `USER_PRIVATE_KEY` | User wallet for testing | | `FORWARDER_ADDRESS` | Deployed contract address (set after deploy) | | `RPC_URL` | Flare network RPC (e.g. Coston2 testnet) | @@ -241,7 +255,6 @@ npx ts-node relayer/index.ts | ------ | ----------------- | ---------------------- | | POST | `/execute` | Execute single payment | | GET | `/nonce/:address` | Get nonce for address | -| GET | `/fee` | Get the relayer fee | ### Run the example Flow @@ -283,13 +296,11 @@ Step 2: Approving FXRP for gasless payments... Step 3: Creating payment request... To: 0x5EEeaD99dFfeB32Fe96baad6554f6E009c8B348a Amount: 0.1 FXRP - Fee: 0.01 FXRP Signed request created: From: 0x0d09ff7630588E05E2449aBD3dDD1D8d146bc5c2 To: 0x5EEeaD99dFfeB32Fe96baad6554f6E009c8B348a Amount: 0.1 FXRP - Fee: 0.01 FXRP Deadline: 2026-02-13T14:14:20.000Z Signature: 0x0cc5532b3c67eb0ef4... diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 6bd33058..7a936092 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -20,6 +20,7 @@ const config: Config = { onBrokenLinks: "throw", markdown: { + mermaid: true, hooks: { onBrokenMarkdownImages: "throw", onBrokenMarkdownLinks: "throw", @@ -232,6 +233,7 @@ const config: Config = { }, } satisfies Preset.ThemeConfig, themes: [ + "@docusaurus/theme-mermaid", [ require.resolve("@easyops-cn/docusaurus-search-local"), { diff --git a/examples/developer-hub-javascript/fxrp-gasless/deploy.ts b/examples/developer-hub-javascript/fxrp-gasless/deploy.ts index fb213335..66b8e79e 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/deploy.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/deploy.ts @@ -6,18 +6,13 @@ // 1. Import the necessary libraries import hre from "hardhat"; -import { erc20Abi } from "viem"; import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; -import type { IERC20Metadata } from "../typechain-types/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata"; - -// 2. Define the default relayer fee (FXRP token base units) -const DEFAULT_RELAYER_FEE = 10000n; async function main(): Promise { const network = hre.network.name; console.log(`\nDeploying GaslessPaymentForwarder to ${network}...`); - // 3. Get deployer account and balance + // 2. Get deployer account and balance const [deployer] = await hre.ethers.getSigners(); console.log(`Deployer address: ${deployer.address}`); @@ -28,31 +23,19 @@ async function main(): Promise { console.log( ` ContractRegistry.getAssetManagerFXRP() -> AssetManager.fAsset()`, ); - console.log(`Relayer fee: ${DEFAULT_RELAYER_FEE} (base units)`); - // 4. Deploy the GaslessPaymentForwarder contract (FXRP fetched from registry) + // 3. Deploy the GaslessPaymentForwarder contract (FXRP fetched from registry) const GaslessPaymentForwarder = await hre.ethers.getContractFactory( "GaslessPaymentForwarder", ); - const forwarder = (await GaslessPaymentForwarder.deploy( - DEFAULT_RELAYER_FEE, - )) as unknown as GaslessPaymentForwarder; + const forwarder = + (await GaslessPaymentForwarder.deploy()) as unknown as GaslessPaymentForwarder; await forwarder.waitForDeployment(); const forwarderAddress = await forwarder.getAddress(); - // 5. Get FXRP token address from forwarder (for display) + // 4. Get FXRP token address from forwarder (for display) const fxrpAddress = await forwarder.fxrp(); - const fxrp = new hre.ethers.Contract( - fxrpAddress, - erc20Abi as unknown as string[], - hre.ethers.provider, - ) as unknown as IERC20Metadata; - const decimals = await fxrp.decimals(); - const relayerFeeFormatted = hre.ethers.formatUnits( - DEFAULT_RELAYER_FEE, - decimals, - ); console.log(`\nGaslessPaymentForwarder deployed to: ${forwarderAddress}`); console.log("\n--- Deployment Summary ---"); @@ -60,16 +43,13 @@ async function main(): Promise { console.log(`Contract: ${forwarderAddress}`); console.log(`FXRP Token: ${fxrpAddress}`); console.log(`Owner: ${deployer.address}`); - console.log( - `Relayer Fee: ${relayerFeeFormatted} FXRP (${DEFAULT_RELAYER_FEE} base units)`, - ); - // 6. Verify contract on block explorer + // 5. Verify contract on block explorer console.log("\nVerifying contract..."); try { await hre.run("verify:verify", { address: forwarderAddress, - constructorArguments: [DEFAULT_RELAYER_FEE.toString()], + constructorArguments: [], }); console.log("Contract verified successfully."); } catch (err) { @@ -79,7 +59,7 @@ async function main(): Promise { } else { console.warn("Verification failed:", msg); console.log( - `\nTo verify manually: npx hardhat verify --network ${network} ${forwarderAddress} "${DEFAULT_RELAYER_FEE}"`, + `\nTo verify manually: npx hardhat verify --network ${network} ${forwarderAddress}`, ); } } @@ -87,7 +67,7 @@ async function main(): Promise { return forwarderAddress; } -// 7. Main entry point for the deployment script +// 6. Main entry point for the deployment script main() .then(() => process.exit(0)) .catch((error) => { diff --git a/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts b/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts index 47af79bf..0e59e121 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/example-usage.ts @@ -88,26 +88,22 @@ async function main(): Promise { process.env.RECIPIENT_ADDRESS || "0x0000000000000000000000000000000000000001"; const amountFXRP = "0.1"; // 0.1 FXRP - const feeFXRP = "0.01"; // 0.01 FXRP relayer fee console.log(`\nStep 3: Creating payment request...`); console.log(` To: ${recipientAddress}`); console.log(` Amount: ${amountFXRP} FXRP`); - console.log(` Fee: ${feeFXRP} FXRP`); const paymentRequest = await createPaymentRequest( wallet, FORWARDER_ADDRESS, recipientAddress, amountFXRP, - feeFXRP, ); console.log(`\n Signed request created:`); console.log(` From: ${paymentRequest.from}`); console.log(` To: ${paymentRequest.to}`); console.log(` Amount: ${paymentRequest.meta.amountFormatted}`); - console.log(` Fee: ${paymentRequest.meta.feeFormatted}`); console.log( ` Deadline: ${new Date(paymentRequest.deadline * 1000).toISOString()}`, ); diff --git a/examples/developer-hub-javascript/fxrp-gasless/payment.ts b/examples/developer-hub-javascript/fxrp-gasless/payment.ts index 1ef72706..a975b417 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/payment.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/payment.ts @@ -7,7 +7,7 @@ */ // 1. Import the necessary libraries -import { ethers, Contract, type Wallet, type Provider } from "ethers"; +import { ethers, Contract, Wallet, Provider } from "ethers"; import { erc20Abi, type TypedDataDomain, type TypedData } from "viem"; import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; @@ -28,7 +28,6 @@ export const PAYMENT_REQUEST_TYPES = { { name: "from", type: "address" as const }, { name: "to", type: "address" as const }, { name: "amount", type: "uint256" as const }, - { name: "fee", type: "uint256" as const }, { name: "nonce", type: "uint256" as const }, { name: "deadline", type: "uint256" as const }, ], @@ -39,7 +38,6 @@ export interface SignPaymentParams { forwarderAddress: string; to: string; amount: bigint; - fee: bigint; nonce: bigint; deadline: number; chainId: bigint; @@ -49,12 +47,10 @@ export interface PaymentRequest { from: string; to: string; amount: string; - fee: string; deadline: number; signature: string; meta: { amountFormatted: string; - feeFormatted: string; nonce: string; chainId: string; }; @@ -118,25 +114,12 @@ export async function getNonce( return await forwarder.getNonce(userAddress); } -// 7. Get the minimum relayer fee from the forwarder contract -export async function getRelayerFee( - provider: Provider, - forwarderAddress: string, -): Promise { - const forwarder = GaslessPaymentForwarder__factory.connect( - forwarderAddress, - provider, - ); - return await forwarder.relayerFee(); -} - -// 8. Sign a payment request using EIP-712 +// 7. Sign a payment request using EIP-712 export async function signPaymentRequest( wallet: Wallet, params: SignPaymentParams, ): Promise { - const { forwarderAddress, to, amount, fee, nonce, deadline, chainId } = - params; + const { forwarderAddress, to, amount, nonce, deadline, chainId } = params; // Build the EIP-712 domain const domain = { @@ -150,7 +133,6 @@ export async function signPaymentRequest( from: wallet.address, to: to, amount: amount, - fee: fee, nonce: nonce, deadline: deadline, }; @@ -165,13 +147,12 @@ export async function signPaymentRequest( return signature; } -// 9. Create a complete payment request ready for submission to a relayer +// 8. Create a complete payment request ready for submission to a relayer export async function createPaymentRequest( wallet: Wallet, forwarderAddress: string, to: string, amount: string | number, - fee: string | number | null = null, deadlineSeconds: number = DEFAULT_DEADLINE_SECONDS, ): Promise { const provider = wallet.provider; @@ -189,14 +170,6 @@ export async function createPaymentRequest( // Get current nonce const nonce = await getNonce(provider, forwarderAddress, wallet.address); - // Get fee (use provided or contract default) - let feeDrops: bigint; - if (fee !== null) { - feeDrops = parseAmount(fee, decimals); - } else { - feeDrops = await getRelayerFee(provider, forwarderAddress); - } - // Use chain block timestamp for deadline (avoids clock skew vs contract's block.timestamp) const block = await provider.getBlock("latest"); const chainTime = block?.timestamp ?? Math.floor(Date.now() / 1000); @@ -210,7 +183,6 @@ export async function createPaymentRequest( forwarderAddress, to, amount: amountDrops, - fee: feeDrops, nonce, deadline, chainId, @@ -220,20 +192,18 @@ export async function createPaymentRequest( from: wallet.address, to: to, amount: amountDrops.toString(), - fee: feeDrops.toString(), deadline: deadline, signature: signature, // Metadata (not part of signature) meta: { amountFormatted: formatAmount(amountDrops, decimals) + " FXRP", - feeFormatted: formatAmount(feeDrops, decimals) + " FXRP", nonce: nonce.toString(), chainId: chainId.toString(), }, }; } -// 10. Approve the forwarder contract to spend FXRP (one-time per user) +// 9. Approve the forwarder contract to spend FXRP (one-time per user) export async function approveFXRP( wallet: Wallet, forwarderAddress: string, @@ -268,7 +238,7 @@ export async function approveFXRP( }; } -// 11. Check user's FXRP balance and allowance +// 10. Check user's FXRP balance and allowance export async function checkUserStatus( provider: Provider, forwarderAddress: string, diff --git a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts index 50d28824..66a2710a 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts @@ -21,7 +21,7 @@ import { type TypedDataDomain, type TypedData, } from "viem"; -import express, { type Request, type Response } from "express"; +import express, { Request, Response } from "express"; import cors from "cors"; import "dotenv/config"; import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; @@ -40,7 +40,6 @@ const PAYMENT_REQUEST_TYPES = { { name: "from", type: "address" as const }, { name: "to", type: "address" as const }, { name: "amount", type: "uint256" as const }, - { name: "fee", type: "uint256" as const }, { name: "nonce", type: "uint256" as const }, { name: "deadline", type: "uint256" as const }, ], @@ -73,7 +72,6 @@ export interface PaymentRequest { from: string; to: string; amount: string; - fee: string; deadline: number; signature: string; } @@ -117,7 +115,6 @@ export class GaslessRelayer { const from = ethers.getAddress(request.from); const to = ethers.getAddress(request.to); const amount = BigInt(request.amount); - const fee = BigInt(request.fee); const deadline = Number(request.deadline); const sig = request.signature; if (typeof sig !== "string" || sig.length < 130) { @@ -129,7 +126,6 @@ export class GaslessRelayer { from, to, amount: amount.toString(), - fee: fee.toString(), deadline, signature, }; @@ -148,7 +144,6 @@ export class GaslessRelayer { from, to, amount, - fee, nonce, deadline, }; @@ -182,7 +177,6 @@ export class GaslessRelayer { from, to, amount, - fee, deadline, signature, ); @@ -209,7 +203,6 @@ export class GaslessRelayer { from, to, amount, - fee, deadline, signature, ); @@ -222,18 +215,27 @@ export class GaslessRelayer { } // Execute the payment - const tx = await this.forwarder.executePayment( - from, - to, - amount, - fee, - deadline, - signature, - { gasLimit }, - ); + let tx: ethers.ContractTransactionResponse; + try { + tx = await this.forwarder.executePayment( + from, + to, + amount, + deadline, + signature, + { gasLimit }, + ); + } catch (sendError) { + throw sendError; + } // Wait for confirmation - const receipt = await tx.wait(); + let receipt: ethers.TransactionReceipt | null; + try { + receipt = await tx.wait(); + } catch (waitError) { + throw waitError; + } return { success: true, @@ -245,7 +247,7 @@ export class GaslessRelayer { // 6. Validate a payment request before submission async validateRequest(request: PaymentRequest): Promise { - const { from, amount, fee, deadline } = request; + const { from, amount, deadline } = request; // Check deadline against chain time (not local clock - avoids skew) const block = await this.provider.getBlock("latest"); @@ -267,7 +269,7 @@ export class GaslessRelayer { // Check sender's FXRP balance const balance: bigint = await fxrp.balanceOf(from); - const totalRequired = BigInt(amount) + BigInt(fee); + const totalRequired = BigInt(amount); if (balance < totalRequired) { throw new Error( `Insufficient FXRP balance. Required: ${ethers.formatUnits(totalRequired, decimals)}, Available: ${ethers.formatUnits(balance, decimals)}`, @@ -284,14 +286,6 @@ export class GaslessRelayer { `Insufficient FXRP allowance. Required: ${ethers.formatUnits(totalRequired, decimals)}, Approved: ${ethers.formatUnits(allowance, decimals)}`, ); } - - // Check minimum fee - const minFee: bigint = await this.forwarder.relayerFee(); - if (BigInt(fee) < minFee) { - throw new Error( - `Fee too low. Minimum: ${ethers.formatUnits(minFee, decimals)} FXRP`, - ); - } } // 7. Get the current nonce for an address @@ -299,12 +293,7 @@ export class GaslessRelayer { return await this.forwarder.getNonce(address); } - // 8. Get the minimum relayer fee - async getRelayerFee(): Promise { - return await this.forwarder.relayerFee(); - } - - // 9. Get the FXRP token decimals + // 8. Get the FXRP token decimals async getTokenDecimals(): Promise { const fxrpAddress: string = await this.forwarder.fxrp(); const fxrp = new Contract( @@ -315,7 +304,7 @@ export class GaslessRelayer { return (await fxrp.decimals()) as number; } - // 10. Check relayer's FLR balance for gas + // 9. Check relayer's FLR balance for gas async getRelayerBalance(): Promise { const balance = await this.provider.getBalance(this.wallet.address); return ethers.formatEther(balance); @@ -345,23 +334,6 @@ async function startServer( }, ); - // Get relayer fee - app.get("/fee", async (_req: Request, res: Response) => { - try { - const [fee, decimals] = await Promise.all([ - relayer.getRelayerFee(), - relayer.getTokenDecimals(), - ]); - res.json({ - fee: fee.toString(), - feeFormatted: ethers.formatUnits(fee, decimals) + " FXRP", - }); - } catch (error) { - const err = error as Error; - res.status(400).json({ error: err.message }); - } - }); - // Execute payment app.post("/execute", async (req: Request, res: Response) => { try { @@ -378,7 +350,6 @@ async function startServer( console.log(`\nRelayer server running on http://localhost:${port}`); console.log(`\nEndpoints:`); console.log(` GET /nonce/:addr - Get nonce for address`); - console.log(` GET /fee - Get relayer fee`); console.log(` POST /execute - Execute single payment`); }); } diff --git a/examples/developer-hub-solidity/GaslessPaymentForwarder.sol b/examples/developer-hub-solidity/GaslessPaymentForwarder.sol index 4ad51f80..518a5918 100644 --- a/examples/developer-hub-solidity/GaslessPaymentForwarder.sol +++ b/examples/developer-hub-solidity/GaslessPaymentForwarder.sol @@ -31,12 +31,10 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { mapping(address => uint256) public nonces; // replay protection per sender mapping(address => bool) public authorizedRelayers; // relayer allowlist - uint256 public relayerFee; // default fee (owner-configurable) - // EIP-712 type hash for PaymentRequest bytes32 public constant PAYMENT_REQUEST_TYPEHASH = keccak256( - "PaymentRequest(address from,address to,uint256 amount,uint256 fee,uint256 nonce,uint256 deadline)" + "PaymentRequest(address from,address to,uint256 amount,uint256 nonce,uint256 deadline)" ); // 2. Contract events @@ -44,26 +42,20 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { address indexed from, address indexed to, uint256 amount, - uint256 fee, uint256 nonce ); event RelayerAuthorized(address indexed relayer, bool authorized); // relayer allowlist changed - event RelayerFeeUpdated(uint256 newFee); // default fee changed // 3. Custom errors error InvalidSignature(); // signer != from error ExpiredRequest(); // block.timestamp > deadline error InvalidNonce(); // nonce mismatch (replay) error UnauthorizedRelayer(); // caller not in allowlist - error InsufficientAllowance(); // user approval < amount + fee + error InsufficientAllowance(); // user approval < amount error ZeroAddress(); // zero address passed - // 4. Constructor that initializes the relayer fee - constructor( - uint256 _relayerFee - ) EIP712("GaslessPaymentForwarder", "1") Ownable(msg.sender) { - relayerFee = _relayerFee; // set initial default fee - } + // 4. Constructor + constructor() EIP712("GaslessPaymentForwarder", "1") Ownable(msg.sender) {} // 5. Returns FXRP token from Flare Contract Registry function fxrp() public view returns (IFAsset) { @@ -76,7 +68,6 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { address from, address to, uint256 amount, - uint256 fee, uint256 deadline, bytes calldata signature ) external nonReentrant { @@ -91,7 +82,6 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { from, to, amount, - fee, currentNonce, deadline ) @@ -109,38 +99,29 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { IFAsset _fxrp = fxrp(); // 10. Check if the allowance is sufficient - uint256 totalAmount = amount + fee; - if (_fxrp.allowance(from, address(this)) < totalAmount) { + if (_fxrp.allowance(from, address(this)) < amount) { revert InsufficientAllowance(); } // 11. Transfer the amount to the recipient _fxrp.safeTransferFrom(from, to, amount); - // 12. Transfer the fee to the relayer - if (fee > 0) { - _fxrp.safeTransferFrom(from, msg.sender, fee); - } - - emit PaymentExecuted(from, to, amount, fee, currentNonce); // log success + emit PaymentExecuted(from, to, amount, currentNonce); // log success } - // 13. Views for off-chain signing / validation + // 12. Views for off-chain signing / validation function getNonce(address account) external view returns (uint256) { return nonces[account]; // current nonce for off-chain signing } - // Get the EIP-712 domain separator function getDomainSeparator() external view returns (bytes32) { return _domainSeparatorV4(); // EIP-712 domain separator } - // Compute the hash of a payment request for signing function getPaymentRequestHash( address from, address to, uint256 amount, - uint256 fee, uint256 nonce, uint256 deadline ) external view returns (bytes32) { @@ -150,7 +131,6 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { from, to, amount, - fee, nonce, deadline ) @@ -158,7 +138,7 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { return _hashTypedDataV4(structHash); // full EIP-712 typed-data hash } - // 15. Set relayer authorization status + // 13. Owner: relayer allowlist function setRelayerAuthorization( address relayer, bool authorized @@ -166,10 +146,4 @@ contract GaslessPaymentForwarder is EIP712, Ownable, ReentrancyGuard { authorizedRelayers[relayer] = authorized; // update allowlist emit RelayerAuthorized(relayer, authorized); } - - // 15. Update the default relayer fee - function setRelayerFee(uint256 _relayerFee) external onlyOwner { - relayerFee = _relayerFee; // update default fee - emit RelayerFeeUpdated(_relayerFee); - } } diff --git a/package-lock.json b/package-lock.json index 16b8fd51..149d7090 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@docusaurus/core": "^3.9.2", "@docusaurus/faster": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", + "@docusaurus/theme-mermaid": "^3.9.2", "@easyops-cn/docusaurus-search-local": "^0.52.3", "@mdx-js/react": "^3.1.1", "@tippyjs/react": "^4.2.6", @@ -277,6 +278,19 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1991,6 +2005,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@bytecodealliance/preview2-shim": { "version": "0.17.8", "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.8.tgz", @@ -1998,6 +2018,45 @@ "dev": true, "license": "(Apache-2.0 WITH LLVM-exception)" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", + "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.1.1", + "@chevrotain/types": "11.1.1", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz", + "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.1.1", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz", + "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz", + "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz", + "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==", + "license": "Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3986,6 +4045,34 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", @@ -4455,6 +4542,23 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -5009,6 +5113,15 @@ "react": ">=16" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", + "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, "node_modules/@module-federation/error-codes": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", @@ -6653,19 +6766,6 @@ } } }, - "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", - "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - } - }, "node_modules/@swagger-api/apidom-reference": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.4.0.tgz", @@ -7226,6 +7326,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -7294,6 +7647,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/gtag.js": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", @@ -9185,6 +9544,33 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chevrotain": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", + "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "11.1.1", + "@chevrotain/gast": "11.1.1", + "@chevrotain/regexp-to-ast": "11.1.1", + "@chevrotain/types": "11.1.1", + "@chevrotain/utils": "11.1.1", + "lodash-es": "4.17.23" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -9500,6 +9886,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -9718,6 +10110,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -10071,143 +10472,671 @@ "cssesc": "bin/cssesc" }, "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" } }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" + "d3-path": "^3.1.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" + "d3-array": "2 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" + "d3-time": "1 - 3" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "license": "MIT", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=12" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "css-tree": "~2.2.0" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debounce": { @@ -10378,6 +11307,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -12256,6 +13194,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -13094,6 +14038,15 @@ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -13663,6 +14616,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -13690,6 +14648,23 @@ "node": ">=6" } }, + "node_modules/langium": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.1.1", + "chevrotain-allstar": "~0.3.1", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "node_modules/latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -13715,6 +14690,12 @@ "shell-quote": "^1.8.3" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14054,6 +15035,12 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14198,6 +15185,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -14696,6 +15695,47 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", + "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^1.0.0", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -16694,6 +17734,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -17254,6 +18306,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -17393,6 +18451,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -17452,6 +18516,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -17567,6 +18637,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/pkijs": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", @@ -17584,6 +18665,22 @@ "node": ">=16.0.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -20375,6 +21472,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -20428,6 +21543,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -21374,6 +22495,12 @@ "postcss": "^8.4.31" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -21715,6 +22842,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -21854,19 +22990,6 @@ "tslib": "2" } }, - "node_modules/tree-sitter": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", - "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0" - } - }, "node_modules/tree-sitter-json": { "version": "0.24.8", "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", @@ -21920,6 +23043,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", @@ -22086,6 +23218,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, "node_modules/undici": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz", @@ -22611,6 +23749,55 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", diff --git a/package.json b/package.json index 35b00e98..55ce60d4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@docusaurus/core": "^3.9.2", "@docusaurus/faster": "^3.9.2", "@docusaurus/preset-classic": "^3.9.2", + "@docusaurus/theme-mermaid": "^3.9.2", "@easyops-cn/docusaurus-search-local": "^0.52.3", "@mdx-js/react": "^3.1.1", "@tippyjs/react": "^4.2.6", diff --git a/src/theme/Mermaid/index.tsx b/src/theme/Mermaid/index.tsx new file mode 100644 index 00000000..4824eef1 --- /dev/null +++ b/src/theme/Mermaid/index.tsx @@ -0,0 +1,97 @@ +/** + * Mermaid Flare theme variables per color mode. + * Docusaurus config cannot set different themeVariables for light/dark, so this component is required. + */ + +import React, { useEffect, useRef, useMemo, type ReactNode } from "react"; +import ErrorBoundary from "@docusaurus/ErrorBoundary"; +import { ErrorBoundaryErrorMessageFallback } from "@docusaurus/theme-common"; +import { useColorMode } from "@docusaurus/theme-common"; +import { + MermaidContainerClassName, + useMermaidRenderResult, + useMermaidThemeConfig, +} from "@docusaurus/theme-mermaid/client"; +import type { Props } from "@theme/Mermaid"; +import type { RenderResult } from "mermaid"; + +import styles from "./styles.module.css"; + +// Minimal theme variables (Flare light/dark). Base theme derives the rest. +const LIGHT_VARS = { + darkMode: false, + background: "#f4f4f4", + primaryColor: "#f4f4f4", + primaryTextColor: "#000000", + primaryBorderColor: "#595959", + lineColor: "#595959", + textColor: "#000000", + actorBkg: "#f4f4f4", + actorBorder: "#595959", + actorTextColor: "#000000", + signalColor: "#595959", + signalTextColor: "#000000", + activationBkgColor: "#e62058", + activationBorderColor: "#595959", +}; + +const DARK_VARS = { + darkMode: true, + background: "#1a1a1a", + primaryColor: "#2a2a2a", + primaryTextColor: "#ffffff", + primaryBorderColor: "#5a5a5a", + lineColor: "#5a5a5a", + textColor: "#ffffff", + actorBkg: "#2a2a2a", + actorBorder: "#5a5a5a", + actorTextColor: "#ffffff", + signalColor: "#5a5a5a", + signalTextColor: "#ffffff", + activationBkgColor: "#8b4d65", + activationBorderColor: "#8b4d65", +}; + +function MermaidRenderResult({ + renderResult, +}: { + renderResult: RenderResult; +}): ReactNode { + const ref = useRef(null); + useEffect(() => { + const div = ref.current!; + renderResult.bindFunctions?.(div); + }, [renderResult]); + return ( +
+ ); +} + +export default function Mermaid({ value }: Props): ReactNode { + const { colorMode } = useColorMode(); + const themeConfig = useMermaidThemeConfig(); + const config = useMemo( + () => ({ + startOnLoad: false, + ...themeConfig?.options, + theme: "base" as const, + themeVariables: colorMode === "dark" ? DARK_VARS : LIGHT_VARS, + }), + [colorMode, themeConfig?.options], + ); + const renderResult = useMermaidRenderResult({ text: value, config }); + + return ( + } + > + {renderResult === null ? null : ( + + )} + + ); +} diff --git a/src/theme/Mermaid/styles.module.css b/src/theme/Mermaid/styles.module.css new file mode 100644 index 00000000..dbfe935c --- /dev/null +++ b/src/theme/Mermaid/styles.module.css @@ -0,0 +1,7 @@ +.container { + max-width: 100%; +} + +.container > svg { + max-width: 100%; +} From acb6fb8f85dafda9f7be55c904200a50f0ab4767 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Wed, 18 Feb 2026 08:47:02 +0200 Subject: [PATCH 05/12] feat(docs): add Mermaid theme configuration --- docusaurus.config.ts | 3 + src/theme/Mermaid/index.tsx | 97 ----------------------------- src/theme/Mermaid/styles.module.css | 7 --- 3 files changed, 3 insertions(+), 104 deletions(-) delete mode 100644 src/theme/Mermaid/index.tsx delete mode 100644 src/theme/Mermaid/styles.module.css diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 7a936092..597456f3 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -231,6 +231,9 @@ const config: Config = { theme: prismThemes.oneDark, darkTheme: prismThemes.oneDark, }, + mermaid: { + theme: { light: "neutral", dark: "dark" }, + }, } satisfies Preset.ThemeConfig, themes: [ "@docusaurus/theme-mermaid", diff --git a/src/theme/Mermaid/index.tsx b/src/theme/Mermaid/index.tsx deleted file mode 100644 index 4824eef1..00000000 --- a/src/theme/Mermaid/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Mermaid Flare theme variables per color mode. - * Docusaurus config cannot set different themeVariables for light/dark, so this component is required. - */ - -import React, { useEffect, useRef, useMemo, type ReactNode } from "react"; -import ErrorBoundary from "@docusaurus/ErrorBoundary"; -import { ErrorBoundaryErrorMessageFallback } from "@docusaurus/theme-common"; -import { useColorMode } from "@docusaurus/theme-common"; -import { - MermaidContainerClassName, - useMermaidRenderResult, - useMermaidThemeConfig, -} from "@docusaurus/theme-mermaid/client"; -import type { Props } from "@theme/Mermaid"; -import type { RenderResult } from "mermaid"; - -import styles from "./styles.module.css"; - -// Minimal theme variables (Flare light/dark). Base theme derives the rest. -const LIGHT_VARS = { - darkMode: false, - background: "#f4f4f4", - primaryColor: "#f4f4f4", - primaryTextColor: "#000000", - primaryBorderColor: "#595959", - lineColor: "#595959", - textColor: "#000000", - actorBkg: "#f4f4f4", - actorBorder: "#595959", - actorTextColor: "#000000", - signalColor: "#595959", - signalTextColor: "#000000", - activationBkgColor: "#e62058", - activationBorderColor: "#595959", -}; - -const DARK_VARS = { - darkMode: true, - background: "#1a1a1a", - primaryColor: "#2a2a2a", - primaryTextColor: "#ffffff", - primaryBorderColor: "#5a5a5a", - lineColor: "#5a5a5a", - textColor: "#ffffff", - actorBkg: "#2a2a2a", - actorBorder: "#5a5a5a", - actorTextColor: "#ffffff", - signalColor: "#5a5a5a", - signalTextColor: "#ffffff", - activationBkgColor: "#8b4d65", - activationBorderColor: "#8b4d65", -}; - -function MermaidRenderResult({ - renderResult, -}: { - renderResult: RenderResult; -}): ReactNode { - const ref = useRef(null); - useEffect(() => { - const div = ref.current!; - renderResult.bindFunctions?.(div); - }, [renderResult]); - return ( -
- ); -} - -export default function Mermaid({ value }: Props): ReactNode { - const { colorMode } = useColorMode(); - const themeConfig = useMermaidThemeConfig(); - const config = useMemo( - () => ({ - startOnLoad: false, - ...themeConfig?.options, - theme: "base" as const, - themeVariables: colorMode === "dark" ? DARK_VARS : LIGHT_VARS, - }), - [colorMode, themeConfig?.options], - ); - const renderResult = useMermaidRenderResult({ text: value, config }); - - return ( - } - > - {renderResult === null ? null : ( - - )} - - ); -} diff --git a/src/theme/Mermaid/styles.module.css b/src/theme/Mermaid/styles.module.css deleted file mode 100644 index dbfe935c..00000000 --- a/src/theme/Mermaid/styles.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.container { - max-width: 100%; -} - -.container > svg { - max-width: 100%; -} From 7bc4f5048341ae5ebe59af6464e327fc9d53e851 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Wed, 18 Feb 2026 09:52:11 +0200 Subject: [PATCH 06/12] fix(deps): remove unnecessary peer dependencies from package-lock.json and add new tree-sitter dependency --- package-lock.json | 138 +++++++++++++++++++++++++++++----------------- 1 file changed, 88 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 149d7090..714ab74c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -180,7 +180,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.0.tgz", "integrity": "sha512-RB9bKgYTVUiOcEb5bOcZ169jiiVW811dCsJoLT19DcbbFmU4QaK0ghSTssij35QBQ3SCOitXOUrHcGgNVwS7sQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.48.0", "@algolia/requester-browser-xhr": "5.48.0", @@ -319,7 +318,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2174,7 +2172,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2197,7 +2194,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2307,7 +2303,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2729,7 +2724,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3529,7 +3523,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/babel": "3.9.2", "@docusaurus/bundler": "3.9.2", @@ -3623,7 +3616,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.9.2.tgz", "integrity": "sha512-DEVIwhbrZZ4ir31X+qQNEQqDWkgCJUV6kiPPAd2MGTY8n5/n0c4B8qA5k1ipF2izwH00JEf0h6Daaut71zzkyw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/types": "3.9.2", "@rspack/core": "^1.5.0", @@ -3752,7 +3744,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4021,7 +4012,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4403,6 +4393,7 @@ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -4427,6 +4418,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4438,6 +4430,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -4448,6 +4441,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4461,6 +4455,7 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -4487,6 +4482,7 @@ "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -4502,6 +4498,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4513,6 +4510,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4526,6 +4524,7 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=12.22" }, @@ -4540,7 +4539,8 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@iconify/types": { "version": "2.0.0", @@ -5100,7 +5100,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -6126,7 +6125,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -6766,6 +6764,18 @@ } } }, + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, "node_modules/@swagger-api/apidom-reference": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.4.0.tgz", @@ -6852,7 +6862,6 @@ "integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -7805,7 +7814,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -8540,7 +8548,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8608,7 +8615,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -8673,7 +8679,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.0.tgz", "integrity": "sha512-aD8EQC6KEman6/S79FtPdQmB7D4af/etcRL/KwiKFKgAE62iU8c5PeEQvpvIcBPurC3O/4Lj78nOl7ZcoazqSw==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.14.0", "@algolia/client-abtesting": "5.48.0", @@ -9216,7 +9221,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9549,7 +9553,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", @@ -10290,7 +10293,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10616,7 +10618,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -11038,7 +11039,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -11216,7 +11216,8 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -11428,6 +11429,7 @@ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -11905,7 +11907,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -11979,6 +11980,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11990,6 +11992,7 @@ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -12007,6 +12010,7 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -12017,6 +12021,7 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -12030,6 +12035,7 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -12040,6 +12046,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12053,6 +12060,7 @@ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -12084,6 +12092,7 @@ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -12097,6 +12106,7 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -12466,7 +12476,8 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-uri": { "version": "3.1.0", @@ -12560,6 +12571,7 @@ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -12672,6 +12684,7 @@ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -12698,6 +12711,7 @@ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -12712,7 +12726,8 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -12832,7 +12847,8 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -12934,6 +12950,7 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -12989,6 +13006,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -13000,6 +13018,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13028,6 +13047,7 @@ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -13044,6 +13064,7 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -13140,7 +13161,8 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/gray-matter": { "version": "4.0.3", @@ -13948,7 +13970,6 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14012,6 +14033,7 @@ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -14556,7 +14578,8 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -14711,6 +14734,7 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -15019,6 +15043,7 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -15058,7 +15083,8 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.uniq": { "version": "4.5.0", @@ -18091,6 +18117,7 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } @@ -18166,6 +18193,7 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -18202,6 +18230,7 @@ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -18218,6 +18247,7 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -18463,6 +18493,7 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -18473,6 +18504,7 @@ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -18709,7 +18741,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -19613,7 +19644,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -20161,6 +20191,7 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -20171,7 +20202,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -20437,7 +20467,6 @@ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" @@ -20587,7 +20616,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -20597,7 +20625,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -20676,7 +20703,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -20728,7 +20754,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -20892,8 +20917,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-immutable": { "version": "4.0.0", @@ -21462,6 +21486,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -21620,7 +21645,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -22806,7 +22830,8 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/thingies": { "version": "2.5.0", @@ -22892,7 +22917,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -22990,6 +23014,19 @@ "tslib": "2" } }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, "node_modules/tree-sitter-json": { "version": "0.24.8", "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", @@ -23068,8 +23105,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -23118,6 +23154,7 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -23209,7 +23246,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23851,7 +23887,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -24337,6 +24372,7 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -24402,7 +24438,8 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/write-file-atomic": { "version": "3.0.3", @@ -24534,6 +24571,7 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, From aa7ea6732df74c79e0bc09534f7cd415f173f18f Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Wed, 18 Feb 2026 10:07:00 +0200 Subject: [PATCH 07/12] refactor(types): improve type imports --- .../fxrp-gasless/payment.ts | 2 +- .../fxrp-gasless/relayer.ts | 30 +++++++------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/examples/developer-hub-javascript/fxrp-gasless/payment.ts b/examples/developer-hub-javascript/fxrp-gasless/payment.ts index a975b417..a2cbc201 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/payment.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/payment.ts @@ -7,7 +7,7 @@ */ // 1. Import the necessary libraries -import { ethers, Contract, Wallet, Provider } from "ethers"; +import { ethers, Contract, type Wallet, type Provider } from "ethers"; import { erc20Abi, type TypedDataDomain, type TypedData } from "viem"; import { GaslessPaymentForwarder__factory } from "../typechain-types/factories/contracts/GaslessPaymentForwarder__factory"; diff --git a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts index 66a2710a..9bc16db2 100644 --- a/examples/developer-hub-javascript/fxrp-gasless/relayer.ts +++ b/examples/developer-hub-javascript/fxrp-gasless/relayer.ts @@ -21,7 +21,7 @@ import { type TypedDataDomain, type TypedData, } from "viem"; -import express, { Request, Response } from "express"; +import express, { type Request, type Response } from "express"; import cors from "cors"; import "dotenv/config"; import type { GaslessPaymentForwarder } from "../typechain-types/contracts/GaslessPaymentForwarder"; @@ -215,27 +215,17 @@ export class GaslessRelayer { } // Execute the payment - let tx: ethers.ContractTransactionResponse; - try { - tx = await this.forwarder.executePayment( - from, - to, - amount, - deadline, - signature, - { gasLimit }, - ); - } catch (sendError) { - throw sendError; - } + const tx = await this.forwarder.executePayment( + from, + to, + amount, + deadline, + signature, + { gasLimit }, + ); // Wait for confirmation - let receipt: ethers.TransactionReceipt | null; - try { - receipt = await tx.wait(); - } catch (waitError) { - throw waitError; - } + const receipt = await tx.wait(); return { success: true, From 8a42ed38085f25d23c09af2d8ff95a93feaa0526 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Wed, 18 Feb 2026 10:30:34 +0200 Subject: [PATCH 08/12] fix(docs): remove meta-transactions tag --- docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx | 2 +- docs/tags.yml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx index 8f620842..9be5d230 100644 --- a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx +++ b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx @@ -4,7 +4,7 @@ tags: [intermediate, fassets, fxrp, meta-transactions] slug: gasless-fxrp-payments description: Transfer FXRP without gas using EIP-712 signed meta-transactions and a relayer. keywords: - [fassets, fxrp, gasless, meta-transactions, eip-712, flare-network, relayer] + [fassets, fxrp, gasless, eip-712, flare-network, relayer] sidebar_position: 4 --- diff --git a/docs/tags.yml b/docs/tags.yml index fbe3ef48..90cb9a90 100644 --- a/docs/tags.yml +++ b/docs/tags.yml @@ -46,8 +46,6 @@ x402: label: x402 fxrp: label: fxrp -meta-transactions: - label: meta-transactions react: label: react wagmi: From 56d43034a436c9de55ae711cf98a2e7c7e27cd7b Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Wed, 18 Feb 2026 10:34:13 +0200 Subject: [PATCH 09/12] feat(docs): add meta-transactions tag to relevant guides and update keywords for clarity --- .../04-gasless-fxrp-payments.mdx | 19 +++++++++---------- .../guides/gasless-usdt0-transfers.mdx | 2 +- docs/tags.yml | 2 ++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx index 9be5d230..902b4819 100644 --- a/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx +++ b/docs/fxrp/token-interactions/04-gasless-fxrp-payments.mdx @@ -3,8 +3,7 @@ title: Gasless FXRP Payments tags: [intermediate, fassets, fxrp, meta-transactions] slug: gasless-fxrp-payments description: Transfer FXRP without gas using EIP-712 signed meta-transactions and a relayer. -keywords: - [fassets, fxrp, gasless, eip-712, flare-network, relayer] +keywords: [fassets, fxrp, gasless, eip-712, flare-network, relayer] sidebar_position: 4 --- @@ -210,14 +209,14 @@ Use `scripts/example-usage.ts` to run the example flow. Create a `.env` file in your project root with the following variables: -| Variable | Description | -| --------------------- | ----------------------------------------------- | -| `PRIVATE_KEY` | Deployer private key (for deployment) | -| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas) | -| `USER_PRIVATE_KEY` | User wallet for testing | -| `FORWARDER_ADDRESS` | Deployed contract address (set after deploy) | -| `RPC_URL` | Flare network RPC (e.g. Coston2 testnet) | -| `RELAYER_URL` | Relayer HTTP URL (e.g. `http://localhost:3000`) | +| Variable | Description | +| --------------------- | --------------------------------------------------- | +| `PRIVATE_KEY` | Deployer private key (for deployment) | +| `RELAYER_PRIVATE_KEY` | Relayer wallet (pays gas) | +| `USER_PRIVATE_KEY` | User wallet private key for testing | +| `FORWARDER_ADDRESS` | Deployed contract address (set after deploy) | +| `RPC_URL` | Flare network RPC | +| `RELAYER_URL` | Relayer HTTP URL (default: `http://localhost:3000`) | ## Deploy and run diff --git a/docs/network/guides/gasless-usdt0-transfers.mdx b/docs/network/guides/gasless-usdt0-transfers.mdx index a813a29d..4a0c5974 100644 --- a/docs/network/guides/gasless-usdt0-transfers.mdx +++ b/docs/network/guides/gasless-usdt0-transfers.mdx @@ -1,6 +1,6 @@ --- title: Gasless USD₮0 Transfers -tags: [intermediate, javascript] +tags: [intermediate, javascript, meta-transactions] authors: [vmmunoza] description: Enable gasless USD₮0 transfers on Flare. keywords: [gasless, usdt0, meta-transactions, flare-network, eip-3009, eip-712] diff --git a/docs/tags.yml b/docs/tags.yml index 90cb9a90..fbe3ef48 100644 --- a/docs/tags.yml +++ b/docs/tags.yml @@ -46,6 +46,8 @@ x402: label: x402 fxrp: label: fxrp +meta-transactions: + label: meta-transactions react: label: react wagmi: From eff2839c5bc64ec7d50b0f334a6ef40c82055d2e Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Mon, 23 Feb 2026 15:23:52 +0200 Subject: [PATCH 10/12] feat(settings): don't use cache for lychee link checker --- lychee.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lychee.toml b/lychee.toml index 6142037f..e457b2c0 100644 --- a/lychee.toml +++ b/lychee.toml @@ -1,7 +1,7 @@ base_url = "https://dev.flare.network/" verbose = "info" no_progress = true -cache = true +cache = false max_cache_age = "2d" cache_exclude_status = ["403", "429", "500.."] timeout = 30 From 40c91a21962b00f388a7267060a1101bd3af10ac Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Mon, 23 Feb 2026 16:24:24 +0200 Subject: [PATCH 11/12] fix(settings): revert changes - use cache in link checker --- lychee.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lychee.toml b/lychee.toml index e457b2c0..6142037f 100644 --- a/lychee.toml +++ b/lychee.toml @@ -1,7 +1,7 @@ base_url = "https://dev.flare.network/" verbose = "info" no_progress = true -cache = false +cache = true max_cache_age = "2d" cache_exclude_status = ["403", "429", "500.."] timeout = 30 From 7c6d2de4cb3784186cc2c64ebf6dd7e00d1f6ca4 Mon Sep 17 00:00:00 2001 From: Kristaps Grinbergs Date: Mon, 23 Feb 2026 16:53:21 +0200 Subject: [PATCH 12/12] feat(docs): update FAssets settings guide to use Flare Wagmi package --- .../4-fassets-settings-node.mdx | 283 ++++++++---------- 1 file changed, 120 insertions(+), 163 deletions(-) diff --git a/docs/fassets/developer-guides/4-fassets-settings-node.mdx b/docs/fassets/developer-guides/4-fassets-settings-node.mdx index c5dd06dc..4b3097ab 100644 --- a/docs/fassets/developer-guides/4-fassets-settings-node.mdx +++ b/docs/fassets/developer-guides/4-fassets-settings-node.mdx @@ -24,9 +24,8 @@ This guide is a perfect first step for developers working with FAssets. - [Node.js](https://nodejs.org/en/download/) - [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/) - [TypeScript](https://www.typescriptlang.org/download/) -- [Ethers.js](https://docs.ethers.org/v5/) -- [TypeChain](https://www.npmjs.com/package/typechain) -- [Flare Periphery Contract Artifacts](https://www.npmjs.com/package/@flarenetwork/flare-periphery-contract-artifacts) +- [Viem](https://viem.sh) +- [Flare Wagmi Periphery Package](https://www.npmjs.com/package/@flarenetwork/flare-wagmi-periphery-package) ## Project Setup @@ -47,140 +46,115 @@ Install the following dependencies: ```bash npm install --save-dev \ typescript \ - typechain \ - ethers \ - @typechain/ethers-v6 \ - @flarenetwork/flare-periphery-contract-artifacts + viem \ + @flarenetwork/flare-wagmi-periphery-package ``` ### Configure TypeScript -Create a config file `tsconfig.json to control TypeScript behavior: +Create a `tsconfig.json` file: ```bash npx tsc --init ``` -Change the `tsconfig.json` file so it can find the Flare generated types: +Update `tsconfig.json` as follows: ```json title="tsconfig.json" { "compilerOptions": { - "target": "ES2020", - "module": "ESNext", + "rootDir": "./scripts", + "outDir": "./dist", + "module": "esnext", "moduleResolution": "node", + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["node"], "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "outDir": "./dist" + "strict": true, + "skipLibCheck": true }, - "include": ["scripts/**/*.ts", "typechain/**/*.ts", "typechain/**/*.d.ts"], + "include": ["scripts/**/*.ts"], "exclude": ["node_modules"] } ``` ### Update Package Configuration -For convenience, add the Typescript build and type generation commands to the `scripts` section of the `package.json` file: +Change `package.json` to use ES modules and add a build script: ```json +"type": "module", "scripts": { - "build": "tsc", - "generate-types": "typechain --target ethers-v6 --out-dir typechain './node_modules/@flarenetwork/flare-periphery-contract-artifacts/coston2/artifacts/contracts/**/*.json'" + "build": "tsc" } ``` -The `build` script will compile the TypeScript code. - -Using the Coston2 network artifacts, the `typechain` generates TypeScript types from the Flare Periphery contracts, which are provided through a [package](https://www.npmjs.com/package/@flarenetwork/flare-periphery-contract-artifacts) containing the necessary contract artifacts. - -Change the `package.json` file to use the `module` type to use ES modules and avoid issues with the `import` statement: - -```json -"type": "module", -``` - -### Generate TypeScript Types - -To generate the TypeScript types, run the following command: - -```bash -npm run generate-types -``` - -It will generate the TypeScript types in the `typechain` directory. - ## Implementation ### Create Script File -First, you must create a file to write the TypeScript code for this guide. - ```bash mkdir scripts touch scripts/fassets-settings.ts ``` -Open the `scripts/fassets-settings.ts` file in your favorite code editor. +Open `scripts/fassets-settings.ts` in your favorite code editor. ### Import Dependencies -Import the ethers library to interact with the blockchain: +Import viem to interact with the blockchain and the `coston2` namespace from the [`@flarenetwork/flare-wagmi-periphery-package`](https://www.npmjs.com/package/@flarenetwork/flare-wagmi-periphery-package), which provides all typed contract ABIs for the Coston2 network: ```typescript -import { ethers } from "ethers"; +import { createPublicClient, http } from "viem"; +import { flareTestnet } from "viem/chains"; +import { coston2 } from "@flarenetwork/flare-wagmi-periphery-package"; ``` -You need to import the [Flare Contracts Registry](/network/guides/flare-contracts-registry) and the [FAssets asset manager](/fassets/reference/IAssetManager) contract factory types: +### Define Constants ```typescript -import { IAssetManager__factory } from "../typechain/factories/IAssetManager__factory.js"; -import { IFlareContractRegistry__factory } from "../typechain/factories/IFlareContractRegistry__factory.js"; +const FLARE_CONTRACT_REGISTRY_ADDRESS = + "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019" as const; +const XRP_USD_FEED_ID = "0x015852502f55534400000000000000000000000000" as const; ``` -### Define Constants - -Define two constants: +### Create a Client -- `COSTON2_RPC_URL`: The RPC URL for the Coston2 network. -- `FLARE_CONTRACT_REGISTRY_ADDRESS`: The address of the [Flare Contract Registry](/network/guides/flare-contracts-registry). +Create a viem public client connected to Coston2: ```typescript -const COSTON2_RPC = "https://coston-api.flare.network/ext/C/rpc"; -const FLARE_CONTRACT_REGISTRY_ADDRESS = - "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019"; +const client = createPublicClient({ + chain: flareTestnet, + transport: http(), +}); ``` ### Get the FAssets FXRP Asset Manager Address -You can get the FAssets FXRP Asset Manager address by calling the `getAssetManagerFXRP` function of the Flare Contract Registry. -After that, create a new ethers provider, connect to the Coston2 network, and connect to the FAssets asset manager contract. +Resolve the FXRP Asset Manager address from the [Flare Contract Registry](/network/guides/flare-contracts-registry): ```typescript -const provider = new ethers.JsonRpcProvider(COSTON2_RPC); - -const flareContractRegistry = IFlareContractRegistry__factory.connect( - FLARE_CONTRACT_REGISTRY_ADDRESS, - provider, -); - -const assetManagerAddress = - await flareContractRegistry.getContractAddressByName("AssetManagerFXRP"); -const assetManager = IAssetManager__factory.connect( - assetManagerAddress, - provider, -); +const assetManagerAddress = await client.readContract({ + address: FLARE_CONTRACT_REGISTRY_ADDRESS, + abi: coston2.iFlareContractRegistryAbi, + functionName: "getContractAddressByName", + args: ["AssetManagerFXRP"], +}); ``` ### Implement Settings Retrieval -Next, you must fetch the FAssets configuration settings using the [`getSettings`](/fassets/reference/IAssetManager#getsettings) function of the FAssets asset manager contract. - -The last step is to get the lot size of FXRP in XRP and print it to the console. +Fetch the FAssets configuration settings using the [`getSettings`](/fassets/reference/IAssetManager#getsettings) function and calculate the FXRP lot size: ```typescript -const settings = await assetManager.getSettings(); +const settings = await client.readContract({ + address: assetManagerAddress, + abi: coston2.iAssetManagerAbi, + functionName: "getSettings", +}); + const lotSizeFXRP = Number(settings.lotSizeAMG) / Math.pow(10, Number(settings.assetDecimals)); console.log("Lot Size (FXRP):", lotSizeFXRP); @@ -192,126 +166,109 @@ The [`getSettings`](/fassets/reference/IAssetManager#getsettings) function retur ## Convert Lot Size to USD -To convert the lot size to USD you need to use the [FTSO](/ftso/overview) to get the [anchor price feed](/ftso/scaling/anchor-feeds/) of XRP/USD and convert the FXRP lot size to USD. - -### Import Dependencies +To convert the lot size to USD, fetch the XRP/USD [anchor price feed](/ftso/scaling/anchor-feeds/) from the [FTSO](/ftso/overview). -Import the `FtsoV2Interface__factory` type which is the factories for the [FTSO contracts](/ftso/solidity-reference/FtsoV2Interface). +### Get the FtsoV2 Address ```typescript -import { FtsoV2Interface__factory } from "../typechain/factories/FtsoV2Interface__factory.js"; +const ftsoAddress = await client.readContract({ + address: FLARE_CONTRACT_REGISTRY_ADDRESS, + abi: coston2.iFlareContractRegistryAbi, + functionName: "getContractAddressByName", + args: ["FtsoV2"], +}); ``` -### Define Constants +### Get the Price Feed -Define the constants for the [XRP/USD feed ID](/ftso/scaling/anchor-feeds). +`getFeedById` is a `payable` function, so use `simulateContract` to call it without sending a transaction: ```typescript -const XRP_USD_FEED_ID = "0x015852502f55534400000000000000000000000000"; -``` - -### Get the Price feed XRP/USD - -You can get the price feed XRP/USD by calling the [`getFeedById`](/ftso/solidity-reference/FtsoV2Interface#getfeedbyid) function of the `FtsoV2` contract. - -```typescript -const registry = IFlareContractRegistry__factory.connect( - FLARE_CONTRACT_REGISTRY_ADDRESS, - provider, -); -const ftsoAddress = - await flareContractRegistry.getContractAddressByName("FtsoV2"); -const ftsoV2 = FtsoV2Interface__factory.connect(ftsoAddress, provider); -const priceFeed = await ftsoV2.getFeedById.staticCall(XRP_USD_FEED_ID); +const { + result: [_value, _decimals, _timestamp], +} = await client.simulateContract({ + address: ftsoAddress, + abi: coston2.ftsoV2InterfaceAbi, + functionName: "getFeedById", + args: [XRP_USD_FEED_ID], + value: 0n, +}); ``` ### Convert Lot Size to USD -Convert the lot size to USD by multiplying the lot size by the price of XRP in USD. - ```typescript -const xrpUsdPrice = Number(priceFeed[0]) / Math.pow(10, Number(priceFeed[1])); +const xrpUsdPrice = Number(_value) / Math.pow(10, Number(_decimals)); const lotValueUSD = lotSizeFXRP * xrpUsdPrice; console.log("XRP/USD Price:", xrpUsdPrice); console.log("Lot value in USD:", lotValueUSD); -console.log("Timestamp:", priceFeed[2].toString()); +console.log("Timestamp:", _timestamp.toString()); ``` ## Putting All Together -To put all together, you have the following code: - ```typescript title="scripts/fassets-settings.ts" -// Importing necessary modules and contract factories -import { ethers } from "ethers"; -import { IAssetManager__factory } from "../typechain/factories/IAssetManager__factory.js"; -import { IFlareContractRegistry__factory } from "../typechain/factories/IFlareContractRegistry__factory.js"; -import { FtsoV2Interface__factory } from "../typechain/factories/FtsoV2Interface__factory.js"; - -// Constants for RPC endpoint and contract addresses -const COSTON2_RPC = "https://coston2-api.flare.network/ext/C/rpc"; // RPC URL for the Coston2 network +import { createPublicClient, http } from "viem"; +import { flareTestnet } from "viem/chains"; +import { coston2 } from "@flarenetwork/flare-wagmi-periphery-package"; + const FLARE_CONTRACT_REGISTRY_ADDRESS = - "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019"; // Address of the Flare Contract Registry -const XRP_USD_FEED_ID = "0x015852502f55534400000000000000000000000000"; // Feed ID for XRP/USD price https://dev.flare.network/ftso/scaling/anchor-feeds - -async function getSettings() { - // Create a provider for interacting with the blockchain - const provider = new ethers.JsonRpcProvider(COSTON2_RPC); - - // Connect to the Flare Contract Registry - const flareContractRegistry = IFlareContractRegistry__factory.connect( - FLARE_CONTRACT_REGISTRY_ADDRESS, - provider, - ); - - // Get the address of the FXRP Asset Manager - const assetManagerAddress = - await flareContractRegistry.getContractAddressByName("AssetManagerFXRP"); - - // Connect to the FXRP Asset Manager - const assetManager = IAssetManager__factory.connect( - assetManagerAddress, - provider, - ); - - // Fetch settings from the Asset Manager contract - const settings = await assetManager.getSettings(); - - // Calculate the lot size in FXRP (Flare XRP) - const lotSizeFXRP = - Number(settings.lotSizeAMG) / Math.pow(10, Number(settings.assetDecimals)); - console.log("Lot Size (FXRP):", lotSizeFXRP); - - // Fetch the address of the FtsoV2 contract from the registry - const ftsoAddress = - await flareContractRegistry.getContractAddressByName("FtsoV2"); - - // Connect to the FtsoV2 contract - const ftsoV2 = FtsoV2Interface__factory.connect(ftsoAddress, provider); - - // Fetch the XRP/USD price feed using the feed ID - const priceFeed = await ftsoV2.getFeedById.staticCall(XRP_USD_FEED_ID); - - // Calculate the XRP/USD price and the lot value in USD - const xrpUsdPrice = Number(priceFeed[0]) / Math.pow(10, Number(priceFeed[1])); - const lotValueUSD = lotSizeFXRP * xrpUsdPrice; - - console.log("XRP/USD Price:", xrpUsdPrice); - console.log("Lot value in USD:", lotValueUSD); - console.log("Timestamp:", priceFeed[2].toString()); -} + "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019" as const; +const XRP_USD_FEED_ID = "0x015852502f55534400000000000000000000000000" as const; + +const client = createPublicClient({ + chain: flareTestnet, + transport: http(), +}); + +const assetManagerAddress = await client.readContract({ + address: FLARE_CONTRACT_REGISTRY_ADDRESS, + abi: coston2.iFlareContractRegistryAbi, + functionName: "getContractAddressByName", + args: ["AssetManagerFXRP"], +}); + +const settings = await client.readContract({ + address: assetManagerAddress, + abi: coston2.iAssetManagerAbi, + functionName: "getSettings", +}); + +const lotSizeFXRP = + Number(settings.lotSizeAMG) / Math.pow(10, Number(settings.assetDecimals)); +console.log("Lot Size (FXRP):", lotSizeFXRP); -getSettings(); +const ftsoAddress = await client.readContract({ + address: FLARE_CONTRACT_REGISTRY_ADDRESS, + abi: coston2.iFlareContractRegistryAbi, + functionName: "getContractAddressByName", + args: ["FtsoV2"], +}); + +const { + result: [_value, _decimals, _timestamp], +} = await client.simulateContract({ + address: ftsoAddress, + abi: coston2.ftsoV2InterfaceAbi, + functionName: "getFeedById", + args: [XRP_USD_FEED_ID], + value: 0n, +}); + +const xrpUsdPrice = Number(_value) / Math.pow(10, Number(_decimals)); +const lotValueUSD = lotSizeFXRP * xrpUsdPrice; + +console.log("XRP/USD Price:", xrpUsdPrice); +console.log("Lot value in USD:", lotValueUSD); +console.log("Timestamp:", _timestamp.toString()); ``` ## Running the Script -To execute the script, run the following commands to compile the TypeScript code and run the script from the `dist` directory: - ```bash npm run build -node dist/scripts/fassets-settings.js +node dist/fassets-settings.js ``` You should see the following output: