diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5d82c4f..d63a22e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -189,6 +189,12 @@ "description": "Onchain social protocol with Neynar API, Frames v2 Mini Apps, and transaction frames. Covers Snapchain architecture, FID registry on OP Mainnet, and Warpcast integration.", "category": "Infrastructure" }, + { + "name": "flare", + "source": "./skills/flare", + "description": "Provides domain knowledge for the Flare network—EVM L1 with enshrined oracles. Covers: general (networks, chain IDs, RPC endpoints, consensus, tooling), FTSO (block-latency price feeds, Scaling anchor feeds, feed IDs, wagmi/viem integration), FDC (attestation types, EVMTransaction, Web2Json, Payment, Merkle proofs), FAssets (FXRP/FBTC wrapped tokens, minting/redemption, agents, collateral), and Smart Accounts (XRPL-to-Flare without FLR, MasterAccountController, Firelight/Upshift vaults, CLI).", + "category": "L2 & Alt-L1" + }, { "name": "foundry", "source": "./skills/foundry", diff --git a/skills/flare/SKILL.md b/skills/flare/SKILL.md new file mode 100644 index 0000000..03ce547 --- /dev/null +++ b/skills/flare/SKILL.md @@ -0,0 +1,570 @@ +--- +name: flare +description: >- + Provides domain knowledge for the Flare network—EVM L1 with enshrined oracles. Covers: general (networks, chain IDs, RPC endpoints, consensus, tooling), FTSO (block-latency price feeds, Scaling anchor feeds, feed IDs, wagmi/viem integration), FDC (attestation types, EVMTransaction, Web2Json, Payment, Merkle proofs), FAssets (FXRP/FBTC wrapped tokens, minting/redemption, agents, collateral), and Smart Accounts (XRPL-to-Flare without FLR, MasterAccountController, Firelight/Upshift vaults, CLI). Use for any Flare development question. +license: Apache-2.0 +compatibility: Claude Code, Cursor, Windsurf, Cline +metadata: + author: flare-foundation + version: "1.0" + chain: flare + category: L2 & Alt-L1 +tags: + - flare + - evm + - layer1 + - ftso + - fdc + - fassets + - fxrp + - xrpl + - oracle + - price-feeds + - attestation + - smart-accounts +--- + +# Part 1 — Flare Network General + +## What Flare Is + +**Flare** is an interoperable, EVM-compatible Layer 1 blockchain engineered for data-rich applications. Its key differentiator is **enshrined data protocols** built into the core protocol — all validators participate in data provision, and the entire network's economic security backs every data feed. + +**Core value propositions:** +- **Enshrined oracles** — FTSO and FDC are protocol-level, not third-party add-ons. +- **Fast finality** — ~1.8 second block time with single-slot finality (Snowman++ consensus). +- **EVM-compatible** — Deploy any Solidity/Vyper contract; supports Cancun hard fork opcodes. +- **Interoperable** — Native bridges to XRP Ledger, Bitcoin, Dogecoin via FAssets; cross-chain data via FDC. + +## What You Probably Got Wrong + +| Mistake | Correct Behaviour | +|---------|------------------| +| Hardcoding contract addresses | Always resolve via `ContractRegistry` — addresses can change. Only the registry itself is stable. | +| Using `evmVersion: "paris"` or omitting it | Flare requires **`evmVersion: "cancun"`** in Hardhat/Foundry compiler settings. | +| Treating FTSO feed values as floats | Feed values are `uint256`; price = `value / 10^decimals`. Use `getFeedByIdInWei` to get 18-decimal scaled values. | +| Calling `getFtsoV2()` on Coston2 testnet | Use `getTestFtsoV2()` on Coston2 — all methods are free `view` calls. `getFtsoV2()` has payable methods for mainnet. | +| Submitting FDC proofs without waiting for finalization | Always check `Relay.isFinalized(200, roundId)` before fetching proof. Typically 90–180s. | +| Using network-wrong imports for periphery contracts | Import path must match the network: `coston2/ContractRegistry.sol`, `flare/ContractRegistry.sol`, etc. | +| Assuming FXRP/AssetManager addresses are constant | They differ per network. Always resolve via `FlareContractsRegistry.getContractAddressByName("AssetManagerFXRP")`. | +| Sending XRPL Smart Account payments to the wrong address | Get operator XRPL addresses from `MasterAccountController.getXrplProviderWallets()` — don't hardcode. | + +## Core Protocols + +| Protocol | What It Does | +|----------|-------------| +| **FTSO** | Decentralized block-latency price feeds (~1.8s), ~100 data providers, stake-weighted selection. Also provides Scaling anchor feeds every 90s. | +| **FDC** | Validates external data (cross-chain transactions, Web2 APIs) via attestation consensus and Merkle proofs. | +| **FAssets** | Trustless wrapped tokens (FXRP, FBTC, FDOGE) enabling XRP, BTC, and DOGE holders to use Flare DeFi. | +| **Smart Accounts** | Account abstraction letting XRPL users interact with Flare without holding FLR. | + +## Networks + +| Network | Role | Native Token | Chain ID | +|---------|------|--------------|----------| +| **Flare Mainnet** | Production | FLR (18 decimals) | 14 | +| **Coston2** | dApp testnet | C2FLR (18 decimals) | 114 | +| **Songbird** | Canary / protocol experiments | SGB (18 decimals) | 19 | +| **Coston** | Protocol testnet | CFLR (18 decimals) | 16 | + +**Development path:** Coston2 → Flare Mainnet for dApps. Coston → Songbird → Coston2 → Flare Mainnet for protocol work. + +## RPC Endpoints + +| Network | HTTPS | WSS | +|---------|-------|-----| +| Flare Mainnet | `https://flare-api.flare.network/ext/C/rpc` | `wss://flare-api.flare.network/ext/C/ws` | +| Coston2 | `https://coston2-api.flare.network/ext/C/rpc` | `wss://coston2-api.flare.network/ext/C/ws` | +| Songbird | `https://songbird-api.flare.network/ext/C/rpc` | `wss://songbird-api.flare.network/ext/C/ws` | +| Coston | `https://coston-api.flare.network/ext/C/rpc` | `wss://coston-api.flare.network/ext/C/ws` | + +## Block Explorers & Faucets + +| Network | Explorer | +|---------|----------| +| Flare Mainnet | https://flare-explorer.flare.network | +| Coston2 | https://coston2-explorer.flare.network | +| Songbird | https://songbird-explorer.flare.network | +| Coston | https://coston-explorer.flare.network | +| Systems Explorer | https://flare-systems-explorer.flare.network | + +- **Coston2 faucet:** https://faucet.flare.network/coston2 (C2FLR, FXRP, USDT0) +- **Coston faucet:** https://faucet.flare.network/coston (CFLR) + +## Technical Properties + +| Property | Value | +|----------|-------| +| Consensus | Snowman++ (single-slot finality) | +| Block time | ~1.8 seconds | +| EVM version | Cancun (set `evmVersion: "cancun"` in Hardhat/Foundry) | +| Address format | 20-byte ECDSA (Ethereum-compatible) | +| Transaction format | EIP-2718; Type 0 (Legacy) and Type 2 (EIP-1559) | +| Fee burning | All transaction fees are burned | +| Native token decimals | 18 | + +## Key npm Packages + +| Package | Use | +|---------|-----| +| `@flarenetwork/flare-periphery-contracts` | Solidity interfaces for Hardhat/Foundry (network-specific: `coston2/`, `flare/`, `songbird/`) | +| `@flarenetwork/flare-periphery-contract-artifacts` | ABI artifacts for offchain scripts (`interfaceToAbi()`) | +| `@flarenetwork/flare-wagmi-periphery-package` | Wagmi/viem typed contract interactions for Flare periphery contracts | + +## Contract Resolution — ContractRegistry Pattern + +**Never hardcode contract addresses.** Always resolve at runtime: + +```solidity +import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol"; + +IFtsoV2 ftso = ContractRegistry.getFtsoV2(); +IFdcHub fdcHub = ContractRegistry.getFdcHub(); +IWNat wnat = ContractRegistry.getWNat(); +``` + +**FlareContractsRegistry** address (stable on all networks): `0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019` *(Last verified: 2026-03-18)* + +Use network-specific imports (`coston2/`, `flare/`, `songbird/`, `coston/`). + +## Developer Quickstart + +```bash +# Hardhat +git clone https://github.com/flare-foundation/flare-hardhat-starter +cd flare-hardhat-starter && npm install +cp .env.example .env +npx hardhat run scripts/your-script.ts --network coston2 + +# Foundry +git clone https://github.com/flare-foundation/flare-foundry-starter +cd flare-foundry-starter && forge install && forge build +forge script script/YourScript.s.sol --rpc-url https://coston2-api.flare.network/ext/C/rpc +``` + +### Hardhat network config + +```js +networks: { + coston2: { url: "https://coston2-api.flare.network/ext/C/rpc", chainId: 114, accounts: [process.env.PRIVATE_KEY] }, + flare: { url: "https://flare-api.flare.network/ext/C/rpc", chainId: 14, accounts: [process.env.PRIVATE_KEY] }, +} +``` + +Always set `evmVersion: "cancun"` in Solidity compiler settings. + +## Wrapped Native Tokens (WNat) + +WNat wraps native tokens (FLR, SGB, etc.) into an ERC-20 required for FTSO delegation and governance voting. + +| Operation | Method | +|-----------|--------| +| Wrap | `deposit()` — `payable`, send native as `msg.value` | +| Unwrap | `withdraw(amount)` — burns WNat, returns native 1:1 | + +Resolve via `ContractRegistry.getWNat()`. + +--- + +# Part 2 — FTSO (Flare Time Series Oracle) + +## What FTSO Is + +The **FTSO** is an enshrined oracle delivering decentralized price feeds. + +**Key properties:** +- **Fast** — block-latency feeds update every **≈1.8 seconds**. +- **Scalable** — up to **1000 feeds** across crypto, equities, and commodities. +- **Decentralized** — ~100 independent data providers per feed, selected by delegated stake. +- **Cost-effective** — block-latency view calls are **free**. + +## Feed Types + +| Type | Update Frequency | Cost | +|------|-----------------|------| +| **Block-latency feeds** | Every block (≈1.8s) | Free (view); small fee for state-changing calls | +| **Scaling (anchor) feeds** | Every 90 seconds | Free to query; minimal gas for onchain Merkle verification | + +## Feed IDs + +Each feed is a **21-byte (`bytes21`) ID**: category byte + UTF-8 ticker, right-padded to 21 bytes. + +| Feed | Feed ID | +|------|---------| +| FLR/USD | `0x01464c522f55534400000000000000000000000000` | +| BTC/USD | `0x014254432f55534400000000000000000000000000` | +| XRP/USD | `0x015852502f55534400000000000000000000000000` | +| ETH/USD | `0x014554482f55534400000000000000000000000000` | +| SGB/USD | `0x015347422f55534400000000000000000000000000` | +| DOGE/USD | `0x01444f47452f555344000000000000000000000000` | +| SOL/USD | `0x01534f4c2f55534400000000000000000000000000` | +| USDC/USD | `0x01555344432f555344000000000000000000000000` | +| USDT/USD | `0x01555344542f555344000000000000000000000000` | +| LINK/USD | `0x014c494e4b2f555344000000000000000000000000` | + +Full feed list: [dev.flare.network/ftso/feeds](https://dev.flare.network/ftso/feeds) + +## Consuming Feeds — Solidity + +Full example: [examples/consume-feeds.sol](examples/consume-feeds.sol) + +Resolve the FTSO contract via `ContractRegistry`, then call `getFeedsById` with an array of feed IDs. Use `getTestFtsoV2()` on Coston2 (free view calls) or `getFtsoV2()` on mainnet (payable). Price = `values[i] / 10^decimals[i]`. + +### Key Interface Methods + +| Method | Returns | Notes | +|--------|---------|-------| +| `getFeedById(bytes21)` | `(uint256 value, int8 decimals, uint64 timestamp)` | May require fee (payable) | +| `getFeedByIdInWei(bytes21)` | `(uint256 value, uint64 timestamp)` | Value scaled to 18 decimals | +| `getFeedsById(bytes21[])` | `(uint256[] values, int8[] decimals, uint64 timestamp)` | Multiple feeds | +| `getFeedsByIdInWei(bytes21[])` | `(uint256[] values, uint64 timestamp)` | Multiple feeds in wei | +| `verifyFeedData(FeedDataWithProof)` | `bool` | Verify Scaling anchor feed against Merkle root | + +### Fee Calculation (when needed) + +```solidity +IFeeCalculator feeCalc = ContractRegistry.getFeeCalculator(); +uint256 fee = feeCalc.calculateFeeByIds(feedIds); +ftsoV2.getFeedsById{value: fee}(feedIds); +``` + +## Consuming Feeds — TypeScript (offchain) + +Full example: [examples/read-feeds-offchain.ts](examples/read-feeds-offchain.ts) + +Uses `web3` + `@flarenetwork/flare-periphery-contract-artifacts`. Resolves FtsoV2 address via ContractRegistry at runtime, then calls `getFeedByIdInWei` for 18-decimal-scaled prices. + +## Consuming Feeds — Frontend (wagmi/viem) + +Full example: [examples/read-feeds-wagmi.tsx](examples/read-feeds-wagmi.tsx) + +```bash +npm install wagmi viem @tanstack/react-query @flarenetwork/flare-wagmi-periphery-package +``` + +Uses `@flarenetwork/flare-wagmi-periphery-package` which exports chain configs, ABIs, and `CONTRACT_REGISTRY_ADDRESS`. Resolve FtsoV2 address via `useReadContract` → `getContractAddressByName("FtsoV2")`, then read feeds with `getFeedByIdInWei`. Set `refetchInterval: 2000` for ~1.8s block time. + +**Key rules:** +- Never hardcode FtsoV2 address — always resolve via ContractRegistry. +- On Coston2, use `getTestFtsoV2()` (all view, no fees). +- `getFeedByIdInWei` / `getFeedsByIdInWei` return values scaled to 18 decimals — use `formatUnits(value, 18)`. + +## Scaling (Anchor Feeds) + +Anchor prices are computed via full commit-reveal every **90 seconds**: + +1. **Commit** → providers submit concealed values +2. **Reveal** → providers reveal; **weighted median** computed +3. **Finalize** → Merkle root stored onchain + +Verify with `ftsoV2.verifyFeedData(feedDataWithProof)`. + +Delegation guide: [dev.flare.network/ftso/overview](https://dev.flare.network/ftso/overview) + +--- + +# Part 3 — FDC (Flare Data Connector) + +## What FDC Is + +The **FDC** is an enshrined oracle validating external data for Flare's EVM state. Data providers reach consensus (50%+ signature weight); verified data is stored in a Merkle tree (root only onchain). Developers fetch proofs from the DA Layer and verify them in contracts. + +## Attestation Types + +| Type | Purpose | Supported Sources | +|------|---------|------------------| +| **EVMTransaction** | Verify transactions + events | ETH, FLR, SGB; testETH, testFLR, testSGB | +| **Web2Json** | Fetch Web2 data, JQ transform, ABI-encoded output | PublicWeb2 | +| **Payment** | Confirm payment on non-EVM chains | BTC, DOGE, XRP | +| **AddressValidity** | Validate address format/checksum | BTC, DOGE, XRPL | +| **ConfirmedBlockHeightExists** | Verify block existence | — | +| **BalanceDecreasingTransaction** | Validate balance-decreasing tx | FAssets-oriented | +| **ReferencedPaymentNonexistence** | Prove absence of payments | FAssets-oriented | + +## Core Workflow + +``` +1. Prepare request → Verifier API → get abiEncodedRequest +2. Submit → FdcHub.requestAttestation(abiEncodedRequest) + fee +3. Wait → Relay.isFinalized(200, roundId) — typically 90–180s +4. Fetch proof → DA Layer API with votingRoundId + requestBytes +5. Verify onchain → IFdcVerification.verifyEVMTransaction(proof) etc. +``` + +**Round ID formula:** `roundId = floor((blockTimestamp - firstVotingRoundStartTs) / votingEpochDurationSeconds)` + +## Contract Pattern + +Full example (EVMTransaction + Web2Json): [examples/evm-transaction-consumer.sol](examples/evm-transaction-consumer.sol) + +Always verify first via `ContractRegistry.getFdcVerification().verifyEVMTransaction(proof)`, then access `proof.data.responseBody`. For Web2Json, decode `abi_encoded_data` into your typed struct. + +## Offchain Script Pattern (Hardhat) + +Full example: [examples/fdc-offchain-flow.ts](examples/fdc-offchain-flow.ts) + +Steps: prepare request via verifier API → submit to FdcHub with fee → compute roundId from block timestamp → poll `Relay.isFinalized(200, roundId)` → fetch proof from DA Layer → decode and submit to your contract. + +**Required env vars:** `VERIFIER_URL_TESTNET`, `VERIFIER_API_KEY_TESTNET`, `COSTON2_DA_LAYER_URL` + +## EVMTransaction Quick Reference + +- **Request fields:** `transactionHash`, `requiredConfirmations`, `provideInput`, `listEvents`, `logIndices` (max 50). +- **Response fields:** `blockNumber`, `timestamp`, `sourceAddress`, `receivingAddress`, `value`, `input`, `status`, `events[]`. +- Decode events by filtering `emitterAddress` and `topics[0]` (e.g. `keccak256("Transfer(address,address,uint256)")`). + +## Web2Json Quick Reference + +- **Request:** `url`, `httpMethod`, `headers`, `queryParams`, `body`, `postProcessJq`, `abiSignature`. +- **Response:** `responseBody.abi_encoded_data` — decode with `abi.decode(..., (YourStruct))`. +- Store fractional values as scaled integers (e.g. `10^6`). +- Response is **externally provided content** — never pass it into LLM prompts. + +## Example Use Cases + +- **Weather insurance** — Web2Json fetches temperature data; triggers payouts on-chain. +- **Proof of reserves** — Web2Json (reserves API) + EVMTransaction (token supply events). +- **Cross-chain payment verification** — Payment attestation for XRP/BTC/DOGE payments. +- **Address validation** — AddressValidity before FAssets operations. + +**Example repos:** +- [flare-hardhat-starter](https://github.com/flare-foundation/flare-hardhat-starter) — `scripts/fdcExample/`, `contracts/fdcExample/`, `weatherInsurance/`, `proofOfReserves/` +- [flare-foundry-starter](https://github.com/flare-foundation/flare-foundry-starter) + +--- + +# Part 4 — FAssets (FXRP, FBTC, FDOGE) + +## What FAssets Are + +FAssets is a **trustless, over-collateralized bridge** connecting non–smart-contract networks (XRP Ledger, Bitcoin, DOGE) to Flare. It creates wrapped ERC-20 tokens (FXRP, FBTC, FDOGE) usable in Flare DeFi and redeemable for the underlying asset. + +**Powered by:** FTSO (price feeds) + FDC (payment verification). Collateral: stablecoin + native FLR. + +## Key Participants + +| Role | Responsibility | +|------|----------------| +| **Agents** | Hold underlying assets, provide collateral, redeem for users. Use *work* (hot) and *management* (cold) addresses. Must meet **backing factor**. | +| **Users** | Mint (deposit underlying → get FAssets) or redeem (burn FAssets → get underlying). | +| **Collateral providers** | Lock FLR in agent's pool; earn share of minting fees. | +| **Liquidators** | Burn FAssets for collateral when agent falls below minimum; earn rewards. | +| **Challengers** | Submit proof of agent violations; earn from vault. | + +## Minting Flow + +1. **Reserve collateral** — `reserveCollateral(agentVault, lots, feeBIPS, executor)` + pay CRF. +2. **Send underlying** — User sends XRP/BTC/DOGE to agent's underlying address with payment reference from `CollateralReserved` event. +3. **FDC attestation** — Get Payment attestation/proof for the underlying transaction. +4. **Execute minting** — `executeMinting(proof, collateralReservationId)` → FAssets minted on Flare. + +**Fees:** CRF (native), Minting Fee (underlying), optional Executor Fee (native). + +## Redemption Flow + +Request redemption → agent pays out underlying on source chain → user receives XRP/BTC/DOGE. + +## Getting Contract Addresses at Runtime + +Full example: [examples/get-fxrp-address.ts](examples/get-fxrp-address.ts) + +Query `FlareContractsRegistry.getContractAddressByName("AssetManagerFXRP")` → attach `IAssetManager` → call `fAsset()` to get the FXRP ERC-20 address. **Never hardcode** AssetManager or FAsset addresses — they differ per network. + +## Reading FAssets Settings + +Full example: [examples/read-fassets-settings.ts](examples/read-fassets-settings.ts) + +Call `getSettings()` on the AssetManager to read `lotSizeAMG` and `assetDecimals`. Lot size in XRP = `lotSizeAMG / 10^assetDecimals`. Combine with FTSO XRP/USD feed to compute lot value in USD. + +In Solidity: `ContractRegistry.getAssetManagerFXRP().getSettings()`. + +Guide: [Read FAssets Settings](https://dev.flare.network/fassets/developer-guides/fassets-settings-node) + +## Gasless FXRP Payments + +FXRP supports **gasless meta-transaction transfers** via EIP-712. Users sign off-chain; a relayer submits on-chain and pays gas. + +Guide: [Gasless FXRP Payments](https://dev.flare.network/fxrp/token-interactions/gasless-fxrp-payments) + +## Terminology + +- **Lot:** Smallest minting unit; size set by AssetManager (e.g. 10 XRP for FXRP). +- **Backing factor:** Minimum collateral ratio agents must maintain. +- **CRF:** Collateral Reservation Fee (paid upfront, non-refundable on failure). +- **UBA:** Smallest underlying asset unit (e.g. XRP drops). +- **Core Vault (CV):** Per-asset vault where agents deposit underlying to free collateral. + +--- + +# Part 5 — Flare Smart Accounts + +## What Smart Accounts Are + +**Smart Accounts** enable XRPL users to interact with Flare **without holding any FLR**. Each XRPL address receives a unique smart account on Flare controlled only by that XRPL address. + +**Benefits:** No FLR required · Single XRPL transaction · Operator-managed gas · FDC proof-based security. + +## How It Works + +1. **XRPL Instruction:** User sends a Payment to the operator, encoding a 32-byte instruction in the memo field. +2. **Proof Generation:** Operator requests a Payment attestation from FDC. +3. **On-Chain Execution:** Operator calls `MasterAccountController.executeTransaction(proof, xrplAddress)`. + +Contract verifies proof, retrieves/creates the smart account, decodes the instruction, executes the action. + +## Payment Reference Structure (32 Bytes) + +| Bytes | Field | Description | +|-------|-------|-------------| +| 1 | Instruction ID | First nibble = type (0-F), second nibble = command (0-F) | +| 2 | Wallet ID | Use 0 if not assigned by operator | +| 3-12 | Value | 10-byte encoded amount | +| 13+ | Parameters | Instruction-specific | + +## Instruction Types + +### FXRP (Type `0x0_`) + +| ID | Action | +|----|--------| +| `0x00` | Collateral reservation (mint FXRP) — bytes 13-14: `agentVaultId` | +| `0x01` | Transfer FXRP — bytes 13-32: recipient Flare address (20 bytes) | +| `0x02` | Redeem FXRP → XRP | + +**Example (mint 1 lot via agent 1):** +`0x0000000000000000000000010001000000000000000000000000000000000000` + +### Firelight Vault (Type `0x1_`) + +| ID | Action | +|----|--------| +| `0x10` | Collateral reservation + deposit to Firelight | +| `0x11` | Deposit FXRP to Firelight | +| `0x12` | Initiate withdrawal from Firelight | +| `0x13` | Claim completed withdrawal | + +Bytes 15-16: `vaultId` for all Firelight instructions. + +### Upshift Vault (Type `0x2_`) + +| ID | Action | +|----|--------| +| `0x20` | Collateral reservation + deposit to Upshift | +| `0x21` | Deposit FXRP to Upshift | +| `0x22` | Request withdrawal (starts waiting period) | +| `0x23` | Claim — value field = date in `YYYYMMDD` format | + +Bytes 15-16: `vaultId` for all Upshift instructions. + +### Custom Instructions (Type `0xff`) + +Execute arbitrary contract calls: + +```solidity +struct CustomCall { + address targetContract; + uint256 value; + bytes data; +} +``` + +Call hash = `bytes32(uint256(keccak256(abi.encode(customCalls))) & ((1 << 240) - 1))` (30-byte truncated keccak256). + +Full example: [examples/custom-instruction.ts](examples/custom-instruction.ts) + +Workflow: encode calldata with `encodeFunctionData` → register via `registerCustomInstruction` → get hash via `encodeCustomInstruction` → build payment reference as `0xff` + walletId (1 byte) + callHash (30 bytes). + +## MasterAccountController Key Functions + +| Function | Purpose | +|----------|---------| +| `getPersonalAccount(xrplAddress)` | Get user's smart account on Flare | +| `getXrplProviderWallets()` | Get operator XRPL addresses to send payments to | +| `getVaults()` | List registered vault addresses and types | +| `getAgentVaults()` | List FAssets agent vaults | +| `registerCustomInstruction(calls)` | Register custom instruction | +| `encodeCustomInstruction(calls)` | Get 30-byte hash for custom instruction | +| `executeTransaction(proof, xrplAddress)` | Execute with FDC proof | + +## TypeScript Integration + +Full example: [examples/smart-account-integration.ts](examples/smart-account-integration.ts) + +Uses viem to read smart account state (`getPersonalAccount`, `getXrplProviderWallets`) and `xrpl` package to send Payment transactions with encoded instructions in the memo field. The `MemoData` must be uppercase hex without `0x` prefix. + +## CLI Tool (smart-accounts-cli) + +```bash +git clone https://github.com/flare-foundation/smart-accounts-cli.git +cd smart-accounts-cli && pip install -r requirements.txt && cp .env.example .env +``` + +### CLI commands + +```bash +# FXRP +./smart_accounts.py encode fxrp-cr --wallet-id 0 --value 1 --agent-vault-id 1 +./smart_accounts.py encode fxrp-transfer --value 10 --recipient-address "0x..." +./smart_accounts.py encode fxrp-redeem --value 1 + +# Firelight / Upshift (same pattern, replace prefix) +./smart_accounts.py encode firelight-cr-deposit --value 1 --agent-vault-id 1 --vault-id 1 +./smart_accounts.py encode upshift-claim --value 20251218 --vault-id 2 # value = YYYYMMDD date + +# Bridge and full pipeline +./smart_accounts.py bridge instruction +./smart_accounts.py encode fxrp-cr --value 1 --agent-vault-id 1 \ + | ./smart_accounts.py bridge instruction - \ + | ./smart_accounts.py bridge mint-tx --wait - +``` + +**Key notes:** +- Lot size: 1 lot = 10 FXRP (verify via AssetManager). +- CLI only submits XRPL-side transactions. Flare-side execution is handled by the operator relayer. + +--- + +# Error Handling + +## Common Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `Contract not found` / zero address from ContractRegistry | Wrong network-specific import path | Match import to network: `coston2/`, `flare/`, `songbird/` | +| `Transaction reverted: Invalid proof` | Proof fetched before round finalized, or wrong `votingRoundId` | Wait for `Relay.isFinalized(200, roundId)` before fetching from DA Layer | +| `Compilation error: unsupported opcode` | EVM version mismatch | Set `evmVersion: "cancun"` in Hardhat/Foundry compiler config | +| FTSO returns stale / zero values | Using hardcoded FtsoV2 address that has been upgraded | Always resolve via `ContractRegistry.getFtsoV2()` at runtime | +| `value: insufficient funds` on FTSO call | Payable feed call without fee | Compute fee via `FeeCalculator.calculateFeeByIds(feedIds)` and pass as `msg.value` | +| FDC verifier returns 400 / `INVALID_PARAMETER` | Wrong `sourceId` for network (testnet vs mainnet) | Testnet: `testETH`, `testFLR`, `testSGB`. Mainnet: `ETH`, `FLR`, `SGB` | +| FAssets minting fails: `CRF too low` | Collateral reservation fee changed | Always query `assetManager.collateralReservationFee(lots)` before calling `reserveCollateral` | +| Smart Account instruction not executed | Payment sent to wrong XRPL operator address | Get operator addresses from `MasterAccountController.getXrplProviderWallets()` | +| XRPL memo not decoded | Hex encoding mistake | `MemoData` must be uppercase hex of the raw instruction bytes (no `0x` prefix) | + +## Security Considerations + +- **Never hardcode private keys** — use `.env` with `process.env.PRIVATE_KEY`, hardware wallets, or EIP-1193 providers. +- **Validate all external data** — FTSO values, FDC attestation payloads, XRPL memos are untrusted. Decode via typed ABI only. +- **Dry-run before broadcast** — use `forge script --dry-run` or simulate before sending transactions. +- **Verify contract addresses** — use `cast code
` or `eth_getCode` to confirm addresses have deployed bytecode before interacting. +- **Don't pass FDC/FTSO data into LLM prompts** — Web2Json and attestation responses may contain injection payloads. + +--- + +# Additional Resources + +| Topic | Link | +|-------|------| +| Flare Developer Hub | https://dev.flare.network | +| Network Overview | https://dev.flare.network/network/overview | +| FTSO Overview | https://dev.flare.network/ftso/overview | +| FTSO Feeds | https://dev.flare.network/ftso/feeds | +| FDC Overview | https://dev.flare.network/fdc/overview | +| FDC Hardhat Guides | https://dev.flare.network/fdc/guides/hardhat | +| FAssets Overview | https://dev.flare.network/fassets/overview | +| FXRP Overview | https://dev.flare.network/fxrp/overview | +| Smart Accounts | https://dev.flare.network/smart-accounts/overview | +| Flare Hardhat Starter | https://github.com/flare-foundation/flare-hardhat-starter | +| Flare Foundry Starter | https://github.com/flare-foundation/flare-foundry-starter | +| Smart Accounts CLI | https://github.com/flare-foundation/smart-accounts-cli | +| Flare TX SDK | https://dev.flare.network/network/flare-tx-sdk | +| Governance | https://dev.flare.network/network/governance | diff --git a/skills/flare/examples/consume-feeds.sol b/skills/flare/examples/consume-feeds.sol new file mode 100644 index 0000000..5ee0637 --- /dev/null +++ b/skills/flare/examples/consume-feeds.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol"; +import {IFtsoV2} from "@flarenetwork/flare-periphery-contracts/coston2/IFtsoV2.sol"; + +contract FtsoConsumer { + bytes21 public constant FLR_USD = 0x01464c522f55534400000000000000000000000000; + bytes21 public constant BTC_USD = 0x014254432f55534400000000000000000000000000; + + function getPrices() external view returns ( + uint256[] memory values, int8[] memory decimals, uint64 timestamp + ) { + IFtsoV2 ftso = ContractRegistry.getTestFtsoV2(); // use getFtsoV2() on mainnet + bytes21[] memory feedIds = new bytes21[](2); + feedIds[0] = FLR_USD; + feedIds[1] = BTC_USD; + return ftso.getFeedsById(feedIds); + // price = values[i] / 10^decimals[i] + } +} diff --git a/skills/flare/examples/custom-instruction.ts b/skills/flare/examples/custom-instruction.ts new file mode 100644 index 0000000..4dd7326 --- /dev/null +++ b/skills/flare/examples/custom-instruction.ts @@ -0,0 +1,113 @@ +// Smart Accounts: register and encode a custom instruction via viem +import { + createPublicClient, + createWalletClient, + encodeFunctionData, + http, + toHex, + type Address, +} from "viem"; +import { flareTestnet } from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; + +const MASTER_ACCOUNT_CONTROLLER_ADDRESS = + "0x..." as Address; // resolve from FlareContractsRegistry + +// Your ABI for MasterAccountController (truncated for brevity) +const masterAccountControllerAbi = [ + { + name: "registerCustomInstruction", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { + name: "customCalls", + type: "tuple[]", + components: [ + { name: "targetContract", type: "address" }, + { name: "value", type: "uint256" }, + { name: "data", type: "bytes" }, + ], + }, + ], + outputs: [], + }, + { + name: "encodeCustomInstruction", + type: "function", + stateMutability: "view", + inputs: [ + { + name: "customCalls", + type: "tuple[]", + components: [ + { name: "targetContract", type: "address" }, + { name: "value", type: "uint256" }, + { name: "data", type: "bytes" }, + ], + }, + ], + outputs: [{ name: "", type: "bytes32" }], + }, +] as const; + +async function main() { + const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY!}`); + const publicClient = createPublicClient({ + chain: flareTestnet, + transport: http(), + }); + const walletClient = createWalletClient({ + account, + chain: flareTestnet, + transport: http(), + }); + + // Build custom instructions + const targetContractAddress = "0x..." as Address; + const targetAbi = [ + { name: "myFunction", type: "function", stateMutability: "nonpayable", inputs: [], outputs: [] }, + ] as const; + + const instructions = [ + { + targetContract: targetContractAddress, + value: BigInt(0), + data: encodeFunctionData({ + abi: targetAbi, + functionName: "myFunction", + args: [], + }), + }, + ]; + + // Register + const { request } = await publicClient.simulateContract({ + account, + address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, + abi: masterAccountControllerAbi, + functionName: "registerCustomInstruction", + args: [instructions], + }); + await walletClient.writeContract(request); + console.log("Custom instruction registered"); + + // Get encoded hash + const encodedInstruction = await publicClient.readContract({ + address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, + abi: masterAccountControllerAbi, + functionName: "encodeCustomInstruction", + args: [instructions], + }); + + // Build payment reference: 0xff + walletId (1 byte) + callHash (30 bytes) + const walletId = 0; + const paymentReference = + "0xff" + + toHex(walletId, { size: 1 }).slice(2) + + (encodedInstruction as string).slice(6); + + console.log("Payment reference for XRPL memo:", paymentReference); +} + +main().catch(console.error); diff --git a/skills/flare/examples/evm-transaction-consumer.sol b/skills/flare/examples/evm-transaction-consumer.sol new file mode 100644 index 0000000..80a64bc --- /dev/null +++ b/skills/flare/examples/evm-transaction-consumer.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol"; +import {IEVMTransaction} from "@flarenetwork/flare-periphery-contracts/coston2/IEVMTransaction.sol"; + +contract EVMTransactionConsumer { + function processProof(IEVMTransaction.Proof calldata proof) external { + require( + ContractRegistry.getFdcVerification().verifyEVMTransaction(proof), + "Invalid proof" + ); + // Safe to use proof.data.responseBody + uint256 blockNumber = proof.data.responseBody.blockNumber; + uint64 timestamp = proof.data.responseBody.timestamp; + // proof.data.responseBody.events[] — decoded events + } +} + +// Web2Json pattern +struct DataTransportObject { + uint256 price; + uint256 timestamp; +} + +contract Web2JsonConsumer { + function processWeb2Proof(IWeb2Json.Proof calldata proof) external { + require( + ContractRegistry.getFdcVerification().verifyWeb2Json(proof), + "Invalid proof" + ); + DataTransportObject memory dto = abi.decode( + proof.data.responseBody.abi_encoded_data, + (DataTransportObject) + ); + // use dto.price, dto.timestamp + } +} diff --git a/skills/flare/examples/fdc-offchain-flow.ts b/skills/flare/examples/fdc-offchain-flow.ts new file mode 100644 index 0000000..b6c0bcc --- /dev/null +++ b/skills/flare/examples/fdc-offchain-flow.ts @@ -0,0 +1,73 @@ +// FDC offchain attestation flow (Hardhat) +// Requires: VERIFIER_URL_TESTNET, VERIFIER_API_KEY_TESTNET, COSTON2_DA_LAYER_URL in .env + +import { ethers } from "hardhat"; + +const VERIFIER_URL = process.env.VERIFIER_URL_TESTNET!; +const VERIFIER_API_KEY = process.env.VERIFIER_API_KEY_TESTNET!; +const DA_LAYER_URL = process.env.COSTON2_DA_LAYER_URL!; + +async function main() { + // 1. Prepare request via verifier + const { abiEncodedRequest } = await fetch( + `${VERIFIER_URL}/verifier/eth/EVMTransaction/prepareRequest`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-apikey": VERIFIER_API_KEY, + }, + body: JSON.stringify({ + attestationType: + "0x45564d5472616e73616374696f6e000000000000000000000000000000000000", + sourceId: + "0x7465737445544800000000000000000000000000000000000000000000000000", + requestBody: { + transactionHash: "0x...", // replace with actual tx hash + requiredConfirmations: "1", + provideInput: true, + listEvents: true, + logIndices: [], + }, + }), + } + ).then((r) => r.json()); + + // 2. Submit to FdcHub + const fdcHub = await ethers.getContractAt("IFdcHub", "0x..."); // resolve via ContractRegistry + const fee = await fdcHub.requestFee(abiEncodedRequest); + const tx = await fdcHub.requestAttestation(abiEncodedRequest, { + value: fee, + }); + const receipt = await tx.wait(); + + // 3. Compute roundId from block timestamp + const block = await ethers.provider.getBlock(receipt!.blockNumber); + const firstVotingRoundStartTs = 0; // get from FlareSystemsManager + const roundId = Math.floor((block!.timestamp - firstVotingRoundStartTs) / 90); + + // 4. Wait for finalization + const relay = await ethers.getContractAt("IRelay", "0x..."); // resolve via ContractRegistry + while (!(await relay.isFinalized(200, roundId))) { + await new Promise((r) => setTimeout(r, 5000)); + } + + // 5. Fetch proof from DA Layer + const proofData = await fetch( + `${DA_LAYER_URL}/api/v1/fdc/proof-by-request-round-raw`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + votingRoundId: roundId, + requestBytes: abiEncodedRequest, + }), + } + ).then((r) => r.json()); + + // 6. Decode response and call your contract + // await myContract.processProof({ merkleProof: proofData.proof, data: decodedResponse }); + console.log("Proof retrieved for round:", roundId); +} + +main().catch(console.error); diff --git a/skills/flare/examples/get-fxrp-address.ts b/skills/flare/examples/get-fxrp-address.ts new file mode 100644 index 0000000..334021c --- /dev/null +++ b/skills/flare/examples/get-fxrp-address.ts @@ -0,0 +1,51 @@ +// Resolve FXRP token address at runtime via FlareContractsRegistry +import { createPublicClient, http, type Address } from "viem"; +import { flareTestnet } from "viem/chains"; + +const REGISTRY: Address = "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019"; + +const registryAbi = [ + { + name: "getContractAddressByName", + type: "function", + stateMutability: "view", + inputs: [{ name: "_name", type: "string" }], + outputs: [{ name: "", type: "address" }], + }, +] as const; + +const assetManagerAbi = [ + { + name: "fAsset", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "address" }], + }, +] as const; + +async function main() { + const client = createPublicClient({ + chain: flareTestnet, + transport: http(), + }); + + // 1. Get FXRP AssetManager address + const assetManagerAddress = await client.readContract({ + address: REGISTRY, + abi: registryAbi, + functionName: "getContractAddressByName", + args: ["AssetManagerFXRP"], + }); + console.log("AssetManager (FXRP):", assetManagerAddress); + + // 2. Get FXRP ERC-20 token address + const fxrpAddress = await client.readContract({ + address: assetManagerAddress, + abi: assetManagerAbi, + functionName: "fAsset", + }); + console.log("FXRP token:", fxrpAddress); +} + +main().catch(console.error); diff --git a/skills/flare/examples/read-fassets-settings.ts b/skills/flare/examples/read-fassets-settings.ts new file mode 100644 index 0000000..4f56877 --- /dev/null +++ b/skills/flare/examples/read-fassets-settings.ts @@ -0,0 +1,98 @@ +// Read FAssets settings (lot size, decimals) and compute lot value in USD +import { createPublicClient, http, type Address } from "viem"; +import { flareTestnet } from "viem/chains"; + +const REGISTRY: Address = "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019"; +const XRP_USD_FEED_ID = + "0x015852502f55534400000000000000000000000000" as `0x${string}`; + +const registryAbi = [ + { + name: "getContractAddressByName", + type: "function", + stateMutability: "view", + inputs: [{ name: "_name", type: "string" }], + outputs: [{ name: "", type: "address" }], + }, +] as const; + +const assetManagerAbi = [ + { + name: "getSettings", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [ + { + name: "", + type: "tuple", + components: [ + { name: "lotSizeAMG", type: "uint256" }, + { name: "assetDecimals", type: "uint8" }, + // ... other fields omitted for brevity + ], + }, + ], + }, +] as const; + +const ftsoV2Abi = [ + { + name: "getFeedById", + type: "function", + stateMutability: "view", + inputs: [{ name: "_feedId", type: "bytes21" }], + outputs: [ + { name: "value", type: "uint256" }, + { name: "decimals", type: "int8" }, + { name: "timestamp", type: "uint64" }, + ], + }, +] as const; + +async function main() { + const client = createPublicClient({ + chain: flareTestnet, + transport: http(), + }); + + // 1. Resolve AssetManager + const assetManagerAddress = await client.readContract({ + address: REGISTRY, + abi: registryAbi, + functionName: "getContractAddressByName", + args: ["AssetManagerFXRP"], + }); + + // 2. Read settings + const settings = await client.readContract({ + address: assetManagerAddress, + abi: assetManagerAbi, + functionName: "getSettings", + }); + + const lotSizeXRP = + Number(settings.lotSizeAMG) / 10 ** settings.assetDecimals; + console.log(`Lot size: ${lotSizeXRP} FXRP`); + + // 3. Get XRP/USD price via FTSO + const ftsoAddress = await client.readContract({ + address: REGISTRY, + abi: registryAbi, + functionName: "getContractAddressByName", + args: ["FtsoV2"], + }); + + const [priceValue, priceDecimals] = await client.readContract({ + address: ftsoAddress, + abi: ftsoV2Abi, + functionName: "getFeedById", + args: [XRP_USD_FEED_ID], + }); + + const xrpUsd = Number(priceValue) / 10 ** Number(priceDecimals); + console.log(`XRP/USD: $${xrpUsd.toFixed(4)}`); + console.log(`Lot value: $${(lotSizeXRP * xrpUsd).toFixed(2)}`); +} + +main().catch(console.error); diff --git a/skills/flare/examples/read-feeds-offchain.ts b/skills/flare/examples/read-feeds-offchain.ts new file mode 100644 index 0000000..25fba39 --- /dev/null +++ b/skills/flare/examples/read-feeds-offchain.ts @@ -0,0 +1,24 @@ +import Web3 from "web3"; +import { interfaceToAbi } from "@flarenetwork/flare-periphery-contract-artifacts"; + +const web3 = new Web3("https://coston2-api.flare.network/ext/C/rpc"); +const REGISTRY = "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019"; + +async function main() { + const registryAbi = interfaceToAbi("IFlareContractRegistry", "coston2"); + const registry = new web3.eth.Contract(registryAbi, REGISTRY); + const ftsoAddress = await registry.methods + .getContractAddressByName("FtsoV2") + .call(); + + const ftsoAbi = interfaceToAbi("IFtsoV2", "coston2"); + const ftso = new web3.eth.Contract(ftsoAbi, ftsoAddress); + + const FLR_USD = "0x01464c522f55534400000000000000000000000000"; + const [value, timestamp] = await ftso.methods + .getFeedByIdInWei(FLR_USD) + .call(); + console.log("FLR/USD (wei-scaled):", value, "timestamp:", timestamp); +} + +main().catch(console.error); diff --git a/skills/flare/examples/read-feeds-wagmi.tsx b/skills/flare/examples/read-feeds-wagmi.tsx new file mode 100644 index 0000000..0d36334 --- /dev/null +++ b/skills/flare/examples/read-feeds-wagmi.tsx @@ -0,0 +1,32 @@ +import { useReadContract } from "wagmi"; +import { + CONTRACT_REGISTRY_ADDRESS, + contractRegistryAbi, + ftsoV2Abi, +} from "@flarenetwork/flare-wagmi-periphery-package"; +import { formatUnits } from "viem"; + +const FLR_USD = "0x01464c522f55534400000000000000000000000000"; + +export function FlrPrice() { + // 1. Resolve FtsoV2 address at runtime + const { data: ftsoV2Address } = useReadContract({ + address: CONTRACT_REGISTRY_ADDRESS, + abi: contractRegistryAbi, + functionName: "getContractAddressByName", + args: ["FtsoV2"], + }); + + // 2. Read feed + const { data } = useReadContract({ + address: ftsoV2Address, + abi: ftsoV2Abi, + functionName: "getFeedByIdInWei", + args: [FLR_USD], + query: { refetchInterval: 2000 }, // ~1.8s block time + }); + + const price = data ? formatUnits(data[0], 18) : "0"; + + return
FLR/USD: ${price}
; +} diff --git a/skills/flare/examples/smart-account-integration.ts b/skills/flare/examples/smart-account-integration.ts new file mode 100644 index 0000000..6743516 --- /dev/null +++ b/skills/flare/examples/smart-account-integration.ts @@ -0,0 +1,81 @@ +// Smart Accounts: read account state and send XRPL payment with encoded instruction +import { createPublicClient, http, type Address } from "viem"; +import { flareTestnet } from "viem/chains"; +import { Client, Wallet } from "xrpl"; + +const MASTER_ACCOUNT_CONTROLLER_ADDRESS = + "0x..." as Address; // resolve from FlareContractsRegistry + +const masterAccountControllerAbi = [ + { + name: "getPersonalAccount", + type: "function", + stateMutability: "view", + inputs: [{ name: "_xrplAddress", type: "string" }], + outputs: [{ name: "", type: "address" }], + }, + { + name: "getXrplProviderWallets", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "string[]" }], + }, +] as const; + +async function main() { + const publicClient = createPublicClient({ + chain: flareTestnet, + transport: http(), + }); + + const xrplAddress = "rYourXRPLAddress"; + + // Get user's smart account address on Flare + const personalAccount = await publicClient.readContract({ + address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, + abi: masterAccountControllerAbi, + functionName: "getPersonalAccount", + args: [xrplAddress], + }); + console.log("Smart account on Flare:", personalAccount); + + // Get operator XRPL addresses + const operatorAddresses = await publicClient.readContract({ + address: MASTER_ACCOUNT_CONTROLLER_ADDRESS, + abi: masterAccountControllerAbi, + functionName: "getXrplProviderWallets", + args: [], + }); + console.log("Operator XRPL addresses:", operatorAddresses); + + // Send XRPL Payment with encoded instruction + const xrplClient = new Client("wss://s.altnet.rippletest.net:51233"); + await xrplClient.connect(); + + const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!); + const encodedInstruction = "0x00..."; // from encode command or manual construction + + const payment = { + TransactionType: "Payment" as const, + Account: xrplWallet.classicAddress, + Destination: operatorAddresses[0], + Amount: "1000000", // instruction fee in drops + Memos: [ + { + Memo: { + MemoData: encodedInstruction.slice(2), // strip 0x prefix, uppercase hex + }, + }, + ], + }; + + const result = await xrplClient.submitAndWait(payment, { + wallet: xrplWallet, + }); + console.log("XRPL tx:", result.result.hash); + + await xrplClient.disconnect(); +} + +main().catch(console.error);