# Stable > Explore Stable docs to integrate stablecoin payments, liquidity, and infrastructure securely into your platform. ## Stable 통합하기 Stable은 USDT0가 네이티브 가스 토큰이자 ERC-20인 레이어 1입니다. 단일 슬롯 완결성, 1초 미만의 블록 타임, 그리고 완전한 EVM 호환성을 갖추고 있습니다. 지갑, 시드 문구, USDT0만 준비하면 됩니다. 구축하려는 기능을 선택하세요. 아래의 모든 경로는 몇 분 안에 테스트넷에서 실행 가능한 가이드로 이어집니다. ### 경로 선택 * [**계정**](/ko/explanation/accounts-overview) — 지갑, EIP-7702 위임, 세션 키, 지출 한도. 사용자 및 에이전트 계정에 대한 최고 수준의 지원. * [**결제**](/ko/explanation/payments-overview) — USDT0 전송, P2P 지갑 구축, 정기 구독, 인보이스 정산, 호출당 과금 API. * [**컨트랙트**](/ko/explanation/contracts-overview) — 컨트랙트 배포, 검증, 인덱싱. Solidity에서 Bank, Distribution, Staking 프리컴파일 호출. * [**AI / 에이전트**](/ko/explanation/agent-settlement) — MCP 서버와 에이전트 스킬을 AI 에디터에 연결. 자율 에이전트를 위한 요청당 API 과금. * [**인프라**](/ko/explanation/integrate-overview) — 가스 면제 서비스, 생태계 제공자(브리지, 오라클, 램프), 네트워크 정보, 노드 운영. * [**학습**](/ko/explanation/learn-overview) — 아키텍처, USDT0 동작, 사용 사례 시나리오, 그리고 Ethereum-to-Stable 레퍼런스. ### 5분 안에 시작하기 * [**빠른 시작**](/ko/tutorial/quick-start) — 연결하고, 포셋에서 지갑에 자금을 충전하고, 0.001 USDT0를 네이티브로 전송하세요. * [**Stable에 연결하기**](/ko/reference/connect) — 체인 ID, RPC 엔드포인트, 포셋, 블록 탐색기. * [**Ethereum과의 차이점**](/ko/explanation/ethereum-comparison) — Ethereum에서 포팅할 때 동일하게 유지되는 것과 변경되는 것. ### 그 외 모든 것 * **네트워크 상태 및 버전**: [테스트넷](/ko/reference/testnet-information) · [메인넷](/ko/reference/mainnet-information) · [버전 기록](/ko/reference/testnet-version-history). * **토크노믹스 및 로드맵**: [STABLE 토크노믹스](/ko/reference/tokenomics) · [기술 로드맵](/ko/explanation/technical-roadmap). * **FAQ**: [개발자 FAQ](/ko/reference/faq) · [개발자 지원](/ko/reference/developer-assistance). ## USDT0을 Stable로 브릿지하기 이 튜토리얼에서는 TypeScript와 ethers v6를 사용하여 Ethereum Sepolia에서 Stable Testnet으로 USDT0를 프로그래밍 방식으로 브릿지합니다. 단계마다 함수를 하나씩 추가하면서 스크립트를 점진적으로 구축합니다. 이 튜토리얼은 OFT Mesh 경로를 사용합니다. Sepolia의 OFT Adapter가 토큰을 잠그고, LayerZero의 이중 DVN 검증이 메시지를 확인하며, Stable에서 USDT0가 발행됩니다. 작동 방식에 대한 전체 설명은 [Stable로 브릿지하기](/ko/explanation/usdt0-bridging)를 참조하세요. :::note 더 적은 코드 줄을 원하시나요? [Stable SDK](/ko/explanation/sdk-overview)는 `quoteBridge`와 `bridge`를 제공하며 경로(LayerZero 또는 LI.FI)를 자동으로 선택합니다. ::: ### 사전 준비 사항 * Node.js 18.0.0 이상 (`node --version`으로 확인) * 직접 제어할 수 있는 개인 키가 있는 Sepolia 지갑 (실제 자금을 보유한 키는 절대 사용하지 마세요) * 가스용 SepoliaETH ([sepoliafaucet.com](https://sepoliafaucet.com) 또는 [faucets.chain.link/sepolia](https://faucets.chain.link/sepolia)에서 받으세요) * 터미널에서 스크립트를 실행하는 기본적인 지식 *** ### 1. 프로젝트 설정 ```bash mkdir stable-bridge && cd stable-bridge npm init -y npm install ethers@6 @layerzerolabs/lz-v2-utilities npm install -D tsx ``` `package.json`에는 다음 내용이 포함되어야 합니다: ```json { "name": "stable-bridge", "version": "1.0.0", "scripts": { "bridge": "tsx --env-file=.env bridge.ts" }, "dependencies": { "@layerzerolabs/lz-v2-utilities": "^2.3.39", "ethers": "^6.13.0" }, "devDependencies": { "tsx": "^4.19.0" } } ``` ### 2. 환경 구성 자격 증명이 담긴 `.env` 파일을 생성하세요: ```bash PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE SEPOLIA_RPC_URL=https://rpc.sepolia.org ``` `SEPOLIA_RPC_URL`의 경우, 다음 중 어느 것이든 작동합니다: * 퍼블릭: `https://rpc.sepolia.org` 또는 `https://ethereum-sepolia-rpc.publicnode.com` * Alchemy: `https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY` * Infura: `https://sepolia.infura.io/v3/YOUR_KEY` ### 3. 스크립트 골격 작성 임포트, 구성, 그리고 `main` 함수를 포함하는 `bridge.ts`를 생성하세요. 다음 단계에서 이 파일에 함수를 추가하고 `main`에서 호출하게 됩니다. ```ts import { ethers, Contract, Wallet, JsonRpcProvider } from "ethers"; import { Options } from "@layerzerolabs/lz-v2-utilities"; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org"; // Contract addresses const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927"; const SEPOLIA_OFT_ADAPTER = "0xc099cD946d5efCC35A99D64E808c1430cEf08126"; const STABLE_USDT0 = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; // Destination: Stable Testnet const STABLE_TESTNET_EID = 40374; // Minimal ABIs — only the functions we call const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function approve(address, uint256) returns (bool)", "function allowance(address, address) view returns (uint256)", "function mint(address, uint256)", ]; const OFT_ADAPTER_ABI = [ "function quoteSend((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), bool) view returns ((uint256 nativeFee, uint256 lzTokenFee))", "function send((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), (uint256 nativeFee, uint256 lzTokenFee), address) payable returns ((bytes32, uint64, (uint256, uint256)), (uint256, uint256))", ]; function addressToBytes32(addr: string): string { return ethers.zeroPadValue(ethers.getBytes(ethers.getAddress(addr)), 32); } // You will add functions here. async function main() { const provider = new JsonRpcProvider(SEPOLIA_RPC_URL); const wallet = new Wallet(PRIVATE_KEY, provider); const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet); const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet); const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals) // You will add function calls here. } main().catch((err) => { console.error(err.message); process.exit(1); }); ``` ### 4. Sepolia에서 테스트 USDT0 발행하기 Sepolia의 테스트 USDT0 컨트랙트는 퍼블릭 `mint` 함수를 노출합니다. `bridge.ts`의 `main` 위에 다음 함수를 추가하세요: ```ts async function mint(usdt0: Contract, receiver: string, amount: bigint) { console.log(`Minting ${ethers.formatEther(amount)} USDT0 on Sepolia...`); const tx = await usdt0.mint(receiver, amount); await tx.wait(); console.log(`Mint tx: ${tx.hash} confirmed`); const balance = await usdt0.balanceOf(receiver); console.log(`USDT0 balance: ${ethers.formatEther(balance)}`); } ``` 그런 다음 `main`에서 호출하세요: ```ts await mint(usdt0, wallet.address, amount); ``` 스크립트를 실행하세요: ```bash npx tsx --env-file=.env bridge.ts ``` *** **체크포인트:** 발행이 확인된 후 0이 아닌 USDT0 잔액이 로그에 표시되어야 합니다. *** ### 5. OFT Adapter 승인하기 OFT Adapter가 토큰을 이동시키려면 ERC-20 allowance가 필요합니다. `main` 위에 이 함수를 추가하세요: ```ts async function approve(usdt0: Contract, spender: string, owner: string, amount: bigint) { console.log("Approving OFT Adapter..."); const tx = await usdt0.approve(spender, amount); await tx.wait(); console.log(`Approve tx: ${tx.hash} confirmed`); const allowance = await usdt0.allowance(owner, spender); console.log(`Allowance: ${ethers.formatEther(allowance)}`); } ``` `main`에서 `mint` 다음에 호출을 추가하세요: ```ts // await mint(usdt0, wallet.address, amount); await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); ``` 스크립트를 실행하세요. 이전 실행에서 이미 토큰이 있다면 `await mint(...)` 호출을 주석 처리할 수 있습니다. *** **체크포인트:** 승인이 확인된 후 스크립트가 0이 아닌 allowance를 로그에 표시해야 합니다. *** ### 6. 수수료 견적 및 브릿지 트랜잭션 전송 `quoteSend` 호출은 LayerZero 메시징 수수료를 SepoliaETH로 반환하며, 이를 `send`에 `msg.value`로 전달합니다. `main` 위에 이 함수를 추가하세요: ```ts async function send(oftAdapter: Contract, receiver: string, amount: bigint) { const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(); const sendParams = { dstEid: STABLE_TESTNET_EID, to: addressToBytes32(receiver), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: "0x", oftCmd: "0x", }; console.log("Quoting bridge fee..."); const feeResult = await oftAdapter.quoteSend(sendParams, false); const fee = { nativeFee: feeResult.nativeFee, lzTokenFee: feeResult.lzTokenFee }; console.log(`Bridge fee: ${ethers.formatEther(fee.nativeFee)} ETH`); console.log("Sending bridge transaction..."); const tx = await oftAdapter.send(sendParams, fee, receiver, { value: fee.nativeFee, }); await tx.wait(); console.log(`Bridge tx: ${tx.hash} confirmed`); console.log(`Sepolia Etherscan: https://sepolia.etherscan.io/tx/${tx.hash}`); console.log(`LayerZero Scan: https://testnet.layerzeroscan.com/tx/${tx.hash}`); } ``` `main`에서 `approve` 다음에 호출을 추가하세요: ```ts // await mint(usdt0, wallet.address, amount); // await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); await send(oftAdapter, wallet.address, amount); ``` ### 7. Stable Testnet에서 도착 확인하기 전송 후, 스크립트는 토큰이 도착할 때까지 Stable Testnet RPC를 폴링할 수 있습니다. `main` 위에 이 함수를 추가하세요: ```ts async function verify(receiver: string) { console.log("Waiting for DVN verification (~2 minutes)..."); const stableProvider = new JsonRpcProvider("https://rpc.testnet.stable.xyz"); const stableUsdt0 = new Contract(STABLE_USDT0, ["function balanceOf(address) view returns (uint256)"], stableProvider); const before: bigint = await stableUsdt0.balanceOf(receiver); for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 5000)); const current: bigint = await stableUsdt0.balanceOf(receiver); if (current > before) { console.log(`\nUSDT0 on Stable: ${ethers.formatEther(current)}`); console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`); return; } process.stdout.write("."); } console.log("\nTokens have not arrived yet. Check manually:"); console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`); } ``` `main`에서 `send` 다음에 호출을 추가하세요: ```ts // await mint(usdt0, wallet.address, amount); // await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); // await send(oftAdapter, wallet.address, amount); await verify(wallet.address); ``` ### 8. 전체 브릿지 실행하기 이제 `main` 함수는 다음과 같아야 합니다: ```ts async function main() { const provider = new JsonRpcProvider(SEPOLIA_RPC_URL); const wallet = new Wallet(PRIVATE_KEY, provider); const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet); const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet); const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals) await mint(usdt0, wallet.address, amount); await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); await send(oftAdapter, wallet.address, amount); await verify(wallet.address); } ``` 실행하세요: ```bash npx tsx --env-file=.env bridge.ts ``` *** **체크포인트:** 다음과 같은 출력이 표시되어야 합니다: ``` Minting 1.0 USDT0 on Sepolia... Mint tx: 0x3a1f...c9d2 confirmed USDT0 balance: 1.0 Approving OFT Adapter... Approve tx: 0x7b2e...f401 confirmed Allowance: 1.0 Quoting bridge fee... Bridge fee: 0.000101 ETH Sending bridge transaction... Bridge tx: 0xa94f...8c11 confirmed Sepolia Etherscan: https://sepolia.etherscan.io/tx/0xa94f...8c11 LayerZero Scan: https://testnet.layerzeroscan.com/tx/0xa94f...8c11 Waiting for DVN verification (~2 minutes)... ...... USDT0 on Stable: 1.0 ``` [Stable Testnet 익스플로러](https://testnet.stablescan.xyz)에서 지갑 주소를 검색하여 발행 이벤트를 확인할 수도 있습니다. *** ### 구축한 내용 Ethereum Sepolia에서 Stable Testnet으로 USDT0를 브릿지했습니다. 이제 다음을 알게 되었습니다: * 컨트랙트의 퍼블릭 `mint` 함수를 사용하여 Sepolia에서 테스트 USDT0 발행하기 * OFT Adapter가 사용자를 대신해 ERC-20 토큰을 사용하도록 승인하기 * 32바이트 주소 인코딩과 executor 옵션으로 LayerZero `sendParams` 구성하기 * 자금을 투입하기 전에 `quoteSend`로 크로스체인 메시징 수수료 견적 받기 * `send`로 크로스체인 토큰 전송을 실행하고 목적지 체인에서 전달 확인하기 * Stable의 RPC(`https://rpc.testnet.stable.xyz`, 체인 ID `2201`)와 Stablescan을 사용하여 온체인 상태 확인하기 ### 다음 추천 * [**첫 USDT0 전송하기**](/ko/tutorial/send-usdt0) — 브릿지된 USDT0를 네이티브 및 ERC-20 전송에 사용하세요. * [**Stable로 브릿지하기**](/ko/explanation/usdt0-bridging) — OFT Mesh와 Legacy Mesh 메커니즘에 대한 심층 분석. * [**테스트넷 정보**](/ko/reference/testnet-information) — 전체 네트워크 파라미터, RPC 엔드포인트, 그리고 faucet 세부 정보. ## 빠른 시작 필요한 도구는 Node.js, 포셋에서 받은 USDT0, 그리고 개인 키뿐입니다. Stable은 USDT0를 가스 토큰으로 사용하므로, 트랜잭션을 처리하는 데 USDT0만 있으면 됩니다. 별도로 충전해야 할 가스 자산은 없습니다. :::note 타입이 지정된 클라이언트를 선호하시나요? [Stable SDK](/ko/explanation/sdk-overview)는 `transfer`, `bridge`, `swap` 메서드로 viem을 감싸므로 수동 ABI 및 소수점 작업을 건너뛸 수 있습니다. ::: ### 사전 준비 * Node.js 20 이상 * 직접 관리하는 개인 키 (새로운 테스트 키도 괜찮습니다) ### 1. 설치 및 구성 프로젝트를 생성하고, `ethers`를 설치한 후, 테스트넷 구성을 저장하세요. ```bash mkdir stable-quickstart && cd stable-quickstart npm init -y && npm install ethers ``` ```text added 1 package, audited 2 packages in 1s ``` 개인 키를 `.env`에 저장하세요: ```bash echo "PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE" > .env ``` `config.ts`를 생성하세요: ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); ``` ### 2. 지갑에 자금 충전 주소를 출력한 다음, 포셋에서 테스트넷 USDT0를 요청하세요. ```typescript // address.ts import { wallet } from "./config"; console.log("Wallet address:", wallet.address); ``` ```bash npx tsx address.ts ``` ```text Wallet address: 0x1234...abcd ``` [https://faucet.stable.xyz](https://faucet.stable.xyz)로 이동하여 주소를 붙여넣고, 버튼을 선택하여 테스트넷 USDT0를 받으세요. 포셋은 1 USDT0를 전송하며, 이는 수천 건의 네이티브 전송에 충분합니다. ### 3. 첫 트랜잭션 전송 0.001 USDT0를 네이티브로 전송하세요. Stable에서 USDT0는 네이티브 자산이므로, 간단한 값 전송이 가장 저렴한 경로입니다(21,000 가스). ```typescript // send.ts import { ethers } from "ethers"; import { provider, wallet } from "./config"; const recipient = "0xRecipientAddress"; // replace with any address const amount = ethers.parseEther("0.001"); // 0.001 USDT0 (18 decimals, native) const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: amount, maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); const receipt = await tx.wait(1); console.log("Tx:", receipt!.hash); console.log("Explorer:", `https://testnet.stablescan.xyz/tx/${receipt!.hash}`); ``` ```bash npx tsx send.ts ``` ```text Tx: 0x8f3a...2d41 Explorer: https://testnet.stablescan.xyz/tx/0x8f3a...2d41 ``` 익스플로러 링크를 열어 트랜잭션을 확인하세요. 블록 생성 시간은 약 0.7초이므로, 이미 최종 확정되었을 것입니다. :::warning `maxPriorityFeePerGas`는 Stable에서 무시되며 반드시 `0`으로 설정해야 합니다. base-fee-only 모델이 트랜잭션 구성을 어떻게 변경하는지는 [가스 가격 책정](/ko/reference/gas-pricing-api)을 참조하세요. ::: ### 다음 단계 * [**스마트 컨트랙트 배포**](/ko/tutorial/smart-contract) — Foundry 프로젝트를 스캐폴딩하고 Stable 테스트넷에 배포하세요. * [**결제 앱 구축**](/ko/how-to/build-p2p-payments) — 지갑 생성, 전송, 수신, 결제 내역 조회를 만들어보세요. * [**AI로 개발하기**](/ko/how-to/develop-with-ai) — MCP 서버와 에이전트 스킬을 AI 에디터에 연결하세요. ## SDK 빠른 시작 `@stablechain/sdk`를 설치하고, 개인 키로 서명하는 클라이언트를 만들고, Stable Testnet에서 USDT0 전송을 보내고, 브리지 및 스왑 견적을 가져옵니다. 총 소요 시간: 약 5분. :::note Stable은 USDT0를 가스 토큰으로 사용합니다. 거래하려면 테스트넷 USDT0만 있으면 됩니다 — 별도로 충전해야 할 네이티브 자산이 없습니다. ::: ### 사전 준비 사항 * Node.js 20 이상 * 테스트넷 USDT0가 있는 테스트 개인 키. [테스트넷 지갑에 자금 충전하기](/ko/how-to/use-faucet)를 참고하세요. ### 1. 설치 ```bash mkdir stable-sdk-quickstart && cd stable-sdk-quickstart npm init -y && npm install @stablechain/sdk viem ``` ```text added 2 packages, audited 3 packages in 2s ``` 테스트 키를 저장합니다: ```bash echo "PRIVATE_KEY=0xYOUR_TEST_KEY" > .env ``` ### 2. 클라이언트 생성 `index.ts`를 생성합니다: ```ts import "dotenv/config"; import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const stable = createStable({ network: Network.Testnet, account, }); console.log("Signer:", account.address); ``` ```text Signer: 0xYourAddress ``` `createStable`은 세 가지 서명 모드를 허용합니다: `account`(서버 측, 위에 표시됨), `transport`(`custom(window.ethereum)`를 통한 브라우저 지갑), 또는 `walletClient`(미리 빌드된 viem `WalletClient`). 세 가지 모두에 대해서는 [viem과 함께 SDK 사용하기](/ko/how-to/sdk-with-viem)를 참고하세요. ### 3. USDT0 전송 보내기 `index.ts`에 추가합니다: ```ts const { txHash } = await stable.transfer({ from: account.address, to: "0x000000000000000000000000000000000000dEaD", amount: 0.001, }); console.log("Transfer:", txHash); ``` 실행합니다: ```bash npx tsx index.ts ``` ```text Signer: 0xYourAddress Transfer: 0x8f3a...2d41 ``` [테스트넷 익스플로러](https://testnet.stablescan.xyz)에서 해시를 열어 확인하세요. ### 4. 브리지 견적 USDT0를 Ethereum Sepolia에서 Stable Testnet으로 브리지합니다. `quoteBridge`는 읽기 전용 호출입니다 — 서명도, 가스도 필요 없습니다: ```ts import { Chain } from "@stablechain/sdk"; const bridgeQuote = await stable.quoteBridge({ fromChain: Chain.Sepolia, toChain: Chain.StableTestnet, fromToken: "0xc4DCC311c028e341fd8602D8eB89c5de94625927", toToken: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", amount: 1, }); console.log("Bridge quote:", bridgeQuote); ``` ```text Bridge quote: { toAmount: 0.999812 } ``` 견적을 `stable.bridge({ ...params, quote })`에 전달하여 실행합니다. SDK는 USDT0 → USDT0 경로에는 LayerZero를, 그 외 모든 경우에는 LI.FI를 선택합니다. ### 5. 스왑 견적 스왑은 LI.FI를 통해 Stable에서 실행됩니다. 견적은 예상 출력값과 미리 빌드된 트랜잭션을 반환합니다: ```ts const swapQuote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", amount: 1, fromDecimals: 6, }); console.log("You'll receive:", swapQuote.toAmount, "USDT0"); ``` ```text You'll receive: 0.998 USDT0 ``` `stable.swap({ ...params, quote: swapQuote })`를 호출하여 실행합니다. ERC-20 소스에 대한 승인은 내부적으로 처리됩니다. ### 다음 추천 * [**SDK 레퍼런스**](/ko/reference/sdk) — 모든 매개변수, 반환 타입, 오류 클래스. * [**viem과 함께 사용하기**](/ko/how-to/sdk-with-viem) — 개인 키, 브라우저 지갑, 미리 빌드된 `WalletClient` 서명 간 전환. * [**wagmi와 함께 사용하기**](/ko/how-to/sdk-with-wagmi) — wagmi 훅을 사용하여 React 앱에 SDK 연결하기. ## 첫 USDT0 보내기 Stable에서 USDT0는 체인의 네이티브 자산이자 ERC-20 토큰입니다. 이는 표준 가치 전송과 함께 `approve`, `transferFrom`, `permit`가 완전히 사용 가능하며, 두 경로 모두 동일한 기본 잔액에서 자금을 이동시킨다는 것을 의미합니다. 이 페이지에서는 두 경로를 통해 USDT0를 보내고 두 방식이 하나의 잔액에서 인출됨을 확인하는 과정을 안내합니다. :::note 타입이 지정된 클라이언트를 선호하시나요? [Stable SDK](/ko/explanation/sdk-overview)는 두 경로를 모두 다루고, 온체인에서 소수점을 처리하며, 지갑의 체인을 자동으로 전환해주는 단일 `transfer({ to, amount, token? })`를 제공합니다. ::: :::note **18 소수점 vs 6 소수점**: 네이티브 USDT0는 18 소수점(표준 EVM 정밀도)을 사용하는 반면, ERC-20 인터페이스는 6 소수점(표준 USDT 정밀도)을 보고합니다. 둘 다 동일한 잔액을 반영하므로, `address(x).balance`와 `USDT0.balanceOf(x)`는 소수점 조정으로 인해 최대 0.000001 USDT0까지 차이가 날 수 있습니다. [Stable에서의 USDT0 동작](/ko/explanation/usdt0-behavior)을 참조하세요. ::: ### 무엇을 만들 것인가 0.001 USDT0를 네이티브 전송으로 보내고, 0.001 USDT0를 ERC-20 전송으로 보낸 다음, 두 잔액을 모두 출력하는 2개의 스크립트 흐름입니다. #### 데모 ```text step 1. Connect wallet → balance displayed 0.01 USDT0 step 2. Send 0.001 USDT0 (choose native or ERC-20 transfer) step 3. Result Sent: 0.001 USDT0 Gas fee: 0.000021 USDT0 Native balance: 0.008979 USDT0 ERC-20 balance: 0.008979 USDT0 ``` ### 사전 준비물 * Node.js 20 이상 * 테스트넷 USDT0가 있는 프라이빗 키. 지갑에 자금을 충전하려면 [빠른 시작](/ko/tutorial/quick-start)을 참조하세요. **USDT0 컨트랙트 주소** * 메인넷: `0x779ded0c9e1022225f8e0630b35a9b54be713736` * 테스트넷: `0x78cf24370174180738c5b8e352b6d14c83a6c9a9` ### 설정 ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); ``` ### 옵션 1 (권장): 네이티브 전송으로 보내기 네이티브 전송은 Ethereum에서 ETH를 보내는 것과 동일하게 작동합니다. `value` 필드가 USDT0 금액을 담습니다. 네이티브 전송은 21,000 가스만 소모하며, USDT0를 보내는 가장 저렴한 방법입니다. ```typescript // sendNative.ts import { ethers } from "ethers"; import { provider, wallet } from "./config"; const recipient = "0xRecipientAddress"; const amount = ethers.parseUnits("0.001", 18); // 18 decimals for native const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: amount, maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); const receipt = await tx.wait(1); console.log("Native transfer tx:", receipt!.hash); ``` ```bash npx tsx sendNative.ts ``` ```text Native transfer tx: 0x8f3a...2d41 ``` ### 옵션 2: ERC-20 전송으로 보내기 USDT0는 ERC-20 전송으로도 보낼 수 있습니다. 이는 동일한 잔액에서 차감되지만, 6 소수점 정밀도를 가진 ERC-20 인터페이스를 사용합니다. ```typescript // sendERC20.ts import { ethers } from "ethers"; import { wallet, USDT0_ADDRESS } from "./config"; const recipient = "0xRecipientAddress"; const amount = ethers.parseUnits("0.001", 6); // 6 decimals for ERC-20 const usdt0 = new ethers.Contract(USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], wallet); const tx = await usdt0.transfer(recipient, amount); const receipt = await tx.wait(1); console.log("ERC-20 transfer tx:", receipt!.hash); ``` ```bash npx tsx sendERC20.ts ``` ```text ERC-20 transfer tx: 0xa2b1...77c0 ``` ### 통합 잔액 확인 두 전송 중 어느 것이든 후에, 두 잔액을 조회하여 동일한 출처에서 인출되는지 확인하세요. ```typescript // balances.ts import { ethers } from "ethers"; import { provider, wallet, USDT0_ADDRESS } from "./config"; const nativeBalance = await provider.getBalance(wallet.address); console.log("Native balance:", ethers.formatEther(nativeBalance), "USDT0"); const usdt0 = new ethers.Contract(USDT0_ADDRESS, [ "function balanceOf(address) view returns (uint256)" ], provider); const erc20Balance = await usdt0.balanceOf(wallet.address); console.log("ERC-20 balance:", ethers.formatUnits(erc20Balance, 6), "USDT0"); ``` ```bash npx tsx balances.ts ``` ```text Native balance: 0.008979 USDT0 ERC-20 balance: 0.008979 USDT0 ``` 두 값은 동일한 잔액을 나타냅니다. [소수점 잔액 조정](/ko/explanation/usdt0-behavior#balance-reconciliation)으로 인해 최대 0.000001 USDT0까지 차이가 날 수 있습니다. ### 다음 권장 사항 * [**가스 제로 트랜잭션**](/ko/how-to/zero-gas-transactions) — 가스 비용을 면제 서비스가 지불하는 방식으로 USDT0를 보냅니다. * [**P2P 결제 앱 만들기**](/ko/how-to/build-p2p-payments) — 지갑 생성, 전송, 수신 및 결제 내역 조회를 구현합니다. * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 이중 역할 잔액 조정과 컨트랙트 설계를 이해합니다. ## 스마트 컨트랙트 배포하기 이 튜토리얼에서는 간단한 스마트 컨트랙트를 Stable 테스트넷에 배포하고 체인에서 해당 상태를 읽어옵니다. 그 과정에서 Stable 네트워크가 어떻게 구성되어 있는지, USDT0가 가스 토큰으로 어떻게 작동하는지, 그리고 표준 EVM 도구를 Stable로 가리키는 방법을 배우게 됩니다. 이 튜토리얼은 Solidity와 Unix 계열 터미널에 대한 기본적인 이해를 전제로 합니다. Stable에 대한 사전 경험은 필요하지 않습니다. ### 무엇을 만들게 되나요 샘플 `Counter` 컨트랙트가 포함된 새로운 Foundry 프로젝트를 Stable 테스트넷에 배포하고, 상태를 변경하는 호출 한 번과 읽기 호출 한 번을 수행합니다. #### 데모 ```text step 1. Scaffold Foundry project → stable-hello/ step 2. Configure testnet RPC: https://rpc.testnet.stable.xyz Chain ID: 2201 step 3. Fund wallet from faucet (1 USDT0) step 4. forge create Counter Deployed to: 0xContract... step 5. cast send Counter.setNumber(42) step 6. cast call Counter.number() → 42 ``` ### 사전 준비 * [Foundry](https://book.getfoundry.sh/getting-started/installation) 설치 (`forge`, `cast`, `anvil`이 PATH에서 사용 가능) * 본인이 제어하는 개인 키가 있는 지갑 (새로운 테스트 키를 사용해도 됩니다. 테스트넷에서는 실제 자금이 있는 키를 절대 사용하지 마세요) * 테스트넷 RPC와 faucet에 접근하기 위한 인터넷 연결 *** ### 1. 새 Foundry 프로젝트 생성하기 다음 명령을 실행하여 새 프로젝트를 스캐폴딩합니다: ```bash forge init stable-hello && cd stable-hello ``` Foundry는 샘플 `Counter.sol` 컨트랙트와 그에 맞는 테스트 파일이 있는 `src/` 디렉터리를 생성합니다. 이 컨트랙트를 있는 그대로 배포할 것입니다. 목표는 새로운 Solidity를 작성하는 것이 아니라 실제로 무언가를 온체인에 올리는 것입니다. ### 2. 배포할 컨트랙트 살펴보기 `src/Counter.sol`을 엽니다. 두 개의 함수가 들어 있습니다: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } ``` `number`는 온체인에 저장되는 public 상태 변수입니다. `increment()`와 `setNumber()`는 이를 변경하는 두 가지 방법입니다. `number`를 읽는 데는 가스가 들지 않습니다. 이는 무료 `eth_call`입니다. ### 3. Stable 테스트넷 구성하기 프로젝트 루트에 `.env`라는 파일을 생성하여 네트워크 자격 증명을 저장합니다: ```bash touch .env ``` 다음 내용을 추가하고, 플레이스홀더를 실제 개인 키로 교체하세요: ```bash PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE ``` 다음으로, `foundry.toml`을 열고 Stable 테스트넷을 이름이 지정된 네트워크 프로필로 추가합니다. 기존 `[profile.default]` 섹션 아래에 이 블록을 추가하세요: ```toml [rpc_endpoints] stable_testnet = "https://rpc.testnet.stable.xyz" ``` 이렇게 하면 `stable_testnet`을 대상으로 할 때 Foundry가 트랜잭션을 어디로 보낼지 알게 됩니다. Stable은 EVM 호환이므로 다른 구성은 필요하지 않습니다. *** **체크포인트:** RPC 엔드포인트에 접근할 수 있는지 확인하세요: ```bash cast chain-id --rpc-url https://rpc.testnet.stable.xyz ``` 예상 출력: ``` 2201 ``` 체인 ID `2201`은 Stable 테스트넷입니다. 이 숫자가 보이면 사용 중인 컴퓨터가 네트워크에 접근할 수 있는 것입니다. *** ### 4. 지갑 주소 가져오기 어떤 계정에 자금을 충전할지 알 수 있도록 개인 키에서 배포자 주소를 도출합니다: ```bash source .env cast wallet address $PRIVATE_KEY ``` 출력된 주소를 복사하세요. 다음 단계에서 필요합니다. ### 5. 지갑에 USDT0 충전하기 Stable은 가스 토큰으로 **USDT0**를 사용합니다. 상품과 서비스 비용을 지불하는 데 사용하는 것과 동일한 자산이 연산 비용을 지불하는 데 직접 사용됩니다. 별도의 네이티브 토큰은 없습니다. 테스트넷 faucet에 방문하여 자금을 요청하세요: ``` https://faucet.stable.xyz ``` 이전 단계의 주소를 붙여넣으세요. faucet은 지갑으로 1 USDT0를 보내며, 이는 여러 컨트랙트를 배포하고 상호작용하기에 충분합니다. *** **체크포인트:** 잔액이 도착했는지 확인하세요: ```bash cast balance $PRIVATE_KEY --rpc-url https://rpc.testnet.stable.xyz ``` 0이 아닌 값이 보여야 합니다. 잔액이 여전히 `0`이면 몇 초 기다린 후 다시 실행하세요. Stable은 약 0.7초마다 새 블록을 생성하므로 자금은 빠르게 정산됩니다. *** ### 6. 컨트랙트 배포하기 `forge create`로 배포를 실행합니다: ```bash source .env forge create src/Counter.sol:Counter \ --rpc-url https://rpc.testnet.stable.xyz \ --private-key $PRIVATE_KEY \ --broadcast ``` Foundry는 컨트랙트를 컴파일하고, 배포 트랜잭션을 브로드캐스트한 후 영수증을 기다립니다. 블록 시간이 약 0.7초이므로 잠깐이면 완료됩니다. *** **체크포인트:** 출력은 다음과 같아야 합니다: ``` [⠒] Compiling... No files changed, compilation skipped Deployer: 0xYourAddress Deployed to: 0xSomeContractAddress Transaction hash: 0xSomeTxHash ``` `Deployed to` 주소를 복사하세요. 다음 두 단계에서 필요합니다. *** ### 7. 쓰기 함수 호출하기 이제 `setNumber()`를 호출하여 값을 온체인에 저장합니다: ```bash cast send 0xSomeContractAddress "setNumber(uint256)" 42 \ --rpc-url https://rpc.testnet.stable.xyz \ --private-key $PRIVATE_KEY ``` 이는 트랜잭션을 전송합니다. 상태 변경에 대해 소액의 USDT0 수수료를 지불하는 것입니다. 이제 값 `42`가 Stable 테스트넷의 `number` 변수에 저장됩니다. ### 8. 체인에서 상태 읽기 `number()`를 호출하여 값을 다시 읽어옵니다. 이는 트랜잭션과 가스가 없는 무료 읽기입니다: ```bash cast call 0xSomeContractAddress "number()(uint256)" \ --rpc-url https://rpc.testnet.stable.xyz ``` 예상 출력: ``` 42 ``` 방금 Stable 테스트넷에 쓰고 읽었습니다. 배포, 쓰기, 읽기의 왕복 과정은 EVM 개발의 핵심 루프이며, 다른 EVM 체인과 동일하게 여기서도 작동합니다. ### 9. Stablescan에서 배포 검사하기 Stable 테스트넷 블록 탐색기를 열고 컨트랙트 주소를 붙여넣으세요: ``` https://testnet.stablescan.xyz ``` 배포 트랜잭션과 방금 수행한 `setNumber` 호출을 볼 수 있습니다. Stablescan은 온체인 상태를 검사하고, 컨트랙트 소스 코드를 검증하고, Stable의 트랜잭션 기록을 읽는 표준 도구입니다. *** ### 무엇을 만들었나요 컨트랙트를 배포하고, 상태를 변경하는 트랜잭션을 보내고, 온체인 상태를 읽었습니다. 모두 Stable 테스트넷에서 수행했습니다. 이제 다음을 할 수 있습니다: * 표준 RPC 엔드포인트를 사용하여 Foundry(또는 모든 EVM 도구 체인)를 Stable을 대상으로 구성하기 * USDT0 faucet을 사용하여 지갑 충전하기 * 가스 토큰인 USDT0로 트랜잭션 비용 지불하기 * Stablescan에서 작업 검사하기 ### 다음 권장 사항 * [**컨트랙트 검증하기**](/ko/how-to/verify-contract) — 사용자가 읽고 상호작용할 수 있도록 소스를 Stablescan에 업로드하세요. * [**컨트랙트 이벤트 인덱싱하기**](/ko/how-to/index-contract) — ethers.js로 이벤트를 구독하고 과거 로그를 백필하세요. * [**가스 가격 책정 참조**](/ko/reference/gas-pricing-api) — USDT0 단위 수수료가 어떻게 계산되는지 이해하세요. ## Brand Kit 아래에서 Stable 브랜드 키트를 확인하실 수 있습니다. 이 키트에는 다양한 형식의 로고와 컬러 팔레트가 포함되어 있으며, 프로젝트나 커뮤니케이션에서 Stable의 브랜딩을 일관되게 유지할 수 있도록 설계되었습니다. [Stable Brand Kit 확인하기](https://www.stable.xyz/brand-kit) ## 퍼실리테이터 퍼실리테이터는 서명된 x402 결제를 검증하고, 이를 Stable에서 USDT0로 정산하는 온체인 호출을 제출합니다. 호스팅된 퍼실리테이터를 사용하면 정산 인프라를 운영하거나 가스 토큰을 관리할 필요가 없습니다. 레일 수준의 맥락은 [에이전트 정산](/ko/explanation/agent-settlement)을 참조하세요. ### 개요 표 | **제공업체** | **카테고리** | **문서 / 시작하기** | **비고** | | :---------------------------------------------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | | [**Semantic Pay**](https://x402.semanticpay.io) | x402 퍼실리테이터 | [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) | Stable용 공개 x402 퍼실리테이터; ERC-3009를 통해 USDT0 결제를 검증하고 정산 | | [**Heurist**](https://facilitator.heurist.xyz) | x402 퍼실리테이터 | [https://docs.heurist.ai/x402-products/facilitator#supported-networks](https://docs.heurist.ai/x402-products/facilitator#supported-networks) | Stable을 지원하는 멀티체인 x402 퍼실리테이터; OFAC 스크리닝을 갖춘 고처리량 검증 및 정산 | ### Semantic Pay AI 에이전트를 위한 결제 인프라: 트러스트리스, P2P, 무허가형. Semantic Pay는 Stable에서 x402 호환 결제 퍼실리테이터로 작동하며, ERC-3009(`transferWithAuthorization`)를 통해 USDT0 결제를 정산합니다. 에이전트는 지갑에 별도의 가스 토큰이 필요하지 않습니다. Stable에서 x402를 통합하는 개발자는 미들웨어를 `https://x402.semanticpay.io`로 지정합니다. 별도의 정산 인프라가 필요하지 않습니다. **기능** * USDT0를 통한 결제 페이로드 검증(`/verify`) 및 온체인 정산(`/settle`) * ERC-3009 `transferWithAuthorization`를 사용한 가스리스 전송 * 에이전트 감독을 위한 지출 한도, 승인 흐름 및 킬 스위치 * 인텐트부터 정산까지 전체 감사 로깅을 갖춘 엔드투엔드 추적성 * 실시간 결제 라이프사이클 업데이트를 위한 이벤트 콜백 **퍼실리테이터 엔드포인트:** `https://x402.semanticpay.io` **문서:** [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) ### Heurist Heurist는 Base, Base Sepolia, X Layer와 함께 Stable을 지원하는 멀티체인 x402 퍼실리테이터를 운영합니다. 초당 수천 건의 결제 검증 및 정산에 맞춰 조정된 처리량으로 고빈도 에이전트 워크로드를 대상으로 합니다. 미들웨어를 `https://facilitator.heurist.xyz`로 지정하세요. 시작하는 데 API 키가 필요하지 않습니다. **기능** * 하나의 엔드포인트에서 여러 네트워크에 걸친 결제 검증 및 온체인 정산 * 고빈도 에이전트 트래픽에 맞춘 처리량 * 발신자 주소의 자동 OFAC 스크리닝 * 검증 및 정산 활동에 대한 실시간 가시성 **퍼실리테이터 엔드포인트:** `https://facilitator.heurist.xyz` **문서:** [https://docs.heurist.ai/x402-products/facilitator#supported-networks](https://docs.heurist.ai/x402-products/facilitator#supported-networks) ### 선택 방법 * 빠르게 시작하려면 호스팅된 퍼실리테이터(Semantic Pay 또는 Heurist)를 사용하세요. 운영할 인프라도, 관리할 가스 토큰도 없습니다. * 워크로드에서 가장 중요한 것을 기준으로 선택하세요: 라이프사이클 콜백과 에이전트 감독 제어 기능을 갖춘 Stable 네이티브 도구를 원한다면 Semantic Pay, Stable과 기타 EVM 네트워크를 아우르는 단일 엔드포인트가 필요하거나 OFAC 스크리닝이 필수 요건이라면 Heurist를 선택하세요. * 정산 정책에 대한 완전한 제어가 필요하거나, 결제 데이터를 자체 환경에 보관하고 싶거나, 운영 부담을 정당화할 만한 볼륨이 예상된다면 자체 호스팅하세요. * 어떤 경로를 선택하든, 프로덕션 트래픽을 보내기 전에 먼저 소액 결제로 테스트하여 검증과 정산이 예상대로 작동하는지 확인하세요. * 두 퍼실리테이터 모두 현재 Stable에서 x402를 정산합니다. MPP의 와이어 포맷은 클라이언트 ↔ 리소스 서버 홉에서만 x402와 다르기 때문에, 동일한 `/settle` 엔드포인트는 MPP 서버의 온체인 제출 대상으로도 사용할 수 있습니다. [Stable에서 MPP 엔드포인트 구축하기](/ko/how-to/build-mpp-endpoint)를 참조하세요. *** Stable과의 에이전트 결제 통합을 보유하고 계신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의하세요. ## 지갑 에이전트 지갑은 AI 에이전트와 자율 시스템에 자기수탁형 서명 기능을 제공하여, 사람이 직접 설정하지 않아도 x402 결제 흐름에 참여할 수 있도록 합니다. ### 개요 표 | **제공자** | **카테고리** | **문서 / 시작하기** | **비고** | | :---------------------------------------------------------------- | :---------- | :------------------------------------------------------------- | :------------------------------------------------------------------- | | [**Wallet Development Kit (WDK)**](https://docs.wallet.tether.io) | 에이전트 지갑 SDK | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether의 오픈소스 SDK; `WalletAccountEvm`이 x402 클라이언트 서명자 인터페이스를 기본적으로 충족 | ### Tether의 Wallet Development Kit (WDK) 자기수탁형 AI 에이전트 지갑을 구축하기 위한 Tether의 오픈소스 SDK입니다. WDK는 에이전트가 클라우드 기반 KMS나 TEE 인프라에 의존하지 않고 로컬에서 개인 키를 생성하고 저장할 수 있도록 합니다. WDK의 `WalletAccountEvm` 인스턴스는 x402 SDK가 요구하는 클라이언트 서명자 인터페이스를 기본적으로 충족합니다. WDK와 Stable의 USDT0를 갖춘 에이전트는 402 HTTP 응답을 자동으로 가로채고, ERC-3009 권한을 서명하며, 요청을 재전송할 수 있습니다. **패키지:** `@tetherto/wdk`, `@tetherto/wdk-wallet-evm` **기능** * 자기수탁형 키 생성 및 로컬 저장 * `WalletAccountEvm`을 통한 기본 x402 클라이언트 서명자 호환성 * 402 응답 자동 가로채기 및 ERC-3009 서명 * Stable을 포함한 멀티체인 지원 **문서:** [https://docs.wallet.tether.io](https://docs.wallet.tether.io) *** Stable과 에이전트 지갑을 통합하셨나요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의해 주세요. ## Bank precompile 참조 :::note **개념:** bank 모듈이 무엇을 하고 언제 사용하는지에 대해서는 [Bank 모듈](/ko/explanation/bank-module)을 참조하세요. ::: ### 개요 Stable SDK의 `x/bank` 모듈은 기본적인 토큰 관리 기능만 제공합니다. 어떤 토큰이든 제한 없이 어떤 계정으로도 전송할 수 있지만, 다른 계정에게 토큰 전송을 위임할 수는 없습니다. 이러한 이유로 `bank` precompiled 컨트랙트는 Stable SDK의 기존 `x/bank` 모듈 위에 추가적인 권한 부여 및 위임 기능을 제공합니다. ### 목차 1. **[개념](#concepts)** 2. **[구성](#configuration)** 3. **[메서드](#methods)** 4. **[이벤트](#events)** ### 개념 이 precompiled 컨트랙트는 ERC-20 표준 메서드를 제공합니다 - 전송을 위한 `transfer`와 `balanceOf`, 위임을 위한 `transferFrom`, `approve`, `allowance` 등이 있습니다. 컨트랙트 주소를 등록하지 않고도 이러한 메서드를 직접 호출할 수 있습니다. 그러나 `mint` 및 `burn` 메서드를 사용하려면 먼저 `x/precompile` 모듈이 컨트랙트 주소를 화이트리스트에 등록해야 합니다. ```go func (p *Precompile) mint( ctx sdk.Context, contract *vm.Contract, denom string, method *abi.Method, stateDB vm.StateDB, args []interface{}, ) ([]byte, error) { // ... // mint method is only allowed for the registered caller contract if _, err := precompilecommon.CheckPermissions(ctx, p.precompileKeeper, contract.CallerAddress, CallerPermissions); err != nil { return nil, err } ``` 이 추가 검증 과정은 이 precompiled 컨트랙트를 호출하는 토큰 컨트랙트가 권한을 부여받았음을 보장합니다. `x/precompile` 모듈 화이트리스트에 토큰 컨트랙트 주소와 그 denom을 등록하려면 거버넌스 제안을 제출해야 합니다. ### 구성 컨트랙트 주소와 가스 비용은 미리 정의되어 있습니다. #### 컨트랙트 주소 * STABLE(거버넌스 토큰)의 경우 `0x0000000000000000000000000000000000001003` ### 메서드 #### `mint` 요청한 양만큼 새로운 토큰을 발행하여 계정으로 전송합니다. 발행할 토큰의 양은 0보다 커야 합니다. 토큰이 성공적으로 발행되어 계정으로 전송되면 `PrecompiledBankMint`가 발생합니다. 참고: * 거버넌스 토큰 발행은 금지되어 있습니다. * mint 메서드를 호출하는 호출자 컨트랙트는 x/precompile 모듈에 등록되어 있어야 합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ------------- | | to | address | 발행된 토큰을 받을 주소 | | amount | uint256 | 발행할 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ----------------------------- | | success | bool | 토큰이 성공적으로 발행되어 계정으로 전송되면 true | #### `burn` 계정에서 요청한 양만큼 토큰을 소각합니다. 소각할 토큰의 양은 0보다 커야 합니다. 토큰이 성공적으로 소각되면 `PrecompiledBankBurn`이 발생합니다. 참고: * 거버넌스 토큰 소각은 금지되어 있습니다. * burn 메서드를 호출하는 호출자 컨트랙트는 x/precompile 모듈에 등록되어 있어야 합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ---------- | | from | address | 토큰을 소각할 주소 | | amount | uint256 | 소각할 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 토큰이 성공적으로 소각되면 true | #### `transfer` 송신자로부터 수신자에게 요청한 양만큼 토큰을 전송합니다. 토큰은 전송 가능하도록 설정되어 있어야 합니다. 전송할 토큰의 양은 0보다 커야 합니다. 토큰이 성공적으로 전송되면 `PrecompiledBankTransfer`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | --------- | | to | address | 토큰을 받을 주소 | | amount | uint256 | 전송할 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 토큰이 성공적으로 전송되면 true | #### `transferFrom` 권한을 부여받은 spender가 allowance 한도 내에서 소유자로부터 수신자에게 요청한 양만큼 토큰을 전송합니다. 토큰은 전송 가능하도록 설정되어 있어야 합니다. 전송할 토큰의 양은 0보다 크고 현재 allowance보다 작거나 같아야 합니다. 토큰이 성공적으로 전송되면 `PrecompiledBankTransfer`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ---------- | | from | address | 토큰을 전송할 주소 | | to | address | 토큰을 받을 주소 | | amount | uint256 | 전송할 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 토큰이 성공적으로 전송되면 true | #### `multiTransfer` 단일 계정에서 여러 계정으로 토큰을 전송합니다. 토큰은 전송 가능하도록 설정되어 있어야 합니다. 각 수신자에게 전송할 토큰의 양은 0보다 커야 합니다. 토큰이 성공적으로 전송되면 각 수신자마다 `PrecompiledBankTransfer`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ---------- | ----------------- | | to | address\[] | 전송된 토큰을 받을 주소들 | | amount | uint256\[] | 각 수신자에게 전송할 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | --------------------------- | | success | bool | 각 수신자에게 토큰이 성공적으로 전송되면 true | #### `approve` 소유자의 계정에서 토큰을 전송할 수 있도록 spender에게 권한을 부여합니다. 권한을 부여할 토큰의 양은 0보다 커야 합니다. 권한이 성공적으로 설정되면 `PrecompiledBankApproval`이 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ------------- | | spender | address | 권한을 부여할 주소 | | value | uint256 | 권한을 부여할 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 권한이 성공적으로 설정되면 true | #### `revoke` 소유자로부터 토큰을 전송할 수 있는 spender의 권한을 취소합니다. 권한이 성공적으로 취소되면 `PrecompiledBankRevoke`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ------ | | spender | address | 취소할 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 권한이 성공적으로 취소되면 true | #### `balanceOf` 계정의 토큰 잔액을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ------------- | | account | address | 토큰 잔액을 조회할 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ------- | ------------ | | balance | uint256 | 계정에 있는 토큰의 양 | #### `totalSupply` 토큰의 총 공급량을 반환합니다. ##### 입력 없음 ##### 출력 | 이름 | 타입 | 설명 | | ----------- | ------- | ------ | | totalSupply | uint256 | 토큰의 총량 | #### `allowance` spender가 owner로부터 아직 인출할 수 있는 양을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ----------- | | owner | address | 소유자의 주소 | | spender | address | spender의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------ | ------- | ------------- | | amount | uint256 | 권한이 부여된 토큰의 양 | ### 이벤트 이 precompiled 컨트랙트에서 발생하는 모든 이벤트는 `PrecompiledBank` 접두사로 시작합니다. 모호함을 피하기 위해, 이 precompiled 컨트랙트를 호출하는 토큰 컨트랙트는 동일한 접두사를 가진 이벤트 이름 사용을 피해야 합니다. #### PrecompiledBankMint | 이름 | 타입 | 인덱싱됨 | 설명 | | ------ | ------- | ---- | ------------- | | from | address | Y | 토큰을 발행한 주소 | | to | address | Y | 발행된 토큰을 받을 주소 | | amount | uint256 | N | 발행된 토큰의 양 | #### PrecompiledBankBurn | 이름 | 타입 | 인덱싱됨 | 설명 | | ------ | ------- | ---- | ---------------- | | from | address | Y | 토큰을 소각한 주소 | | to | address | Y | 이 메서드에서는 사용되지 않음 | | amount | uint256 | N | 소각된 토큰의 양 | #### PrecompiledBankTransfer | 이름 | 타입 | 인덱싱됨 | 설명 | | ------ | ------- | ---- | ------------- | | from | address | Y | 토큰을 전송한 주소 | | to | address | Y | 전송된 토큰을 받을 주소 | | amount | uint256 | N | 전송된 토큰의 양 | #### PrecompiledBankApproval | 이름 | 타입 | 인덱싱됨 | 설명 | | ------- | ------- | ---- | ------------- | | owner | address | Y | 토큰 권한을 부여한 주소 | | spender | address | Y | 권한을 부여할 주소 | | value | uint256 | N | 권한이 부여된 토큰의 양 | #### PrecompiledBankRevoke | 이름 | 타입 | 인덱싱됨 | 설명 | | ------- | ------- | ---- | ------------- | | owner | address | Y | 토큰 권한을 취소한 주소 | | spender | address | Y | 취소할 주소 | | value | uint256 | N | 권한이 부여된 토큰의 양 | ## 브리지 Stable로 USDT0를 송수신하는 것을 지원하는 브리지 제공자입니다. 크로스체인 USDT0 이동이 어떻게 작동하는지는 [Stable로 브리징하기](/ko/explanation/usdt0-bridging)를 참조하세요. 실습 가이드는 [USDT0를 Stable 테스트넷으로 브리징하기](/ko/tutorial/bridge-usdt0) 튜토리얼을 참조하세요. *** ### 지원되는 소스 체인 USDT0가 있는 모든 체인은 OFT 메시를 통해 Stable로 브리징할 수 있습니다. 네이티브 USDT가 있는 모든 체인은 Arbitrum 허브를 통해 레거시 메시로 라우팅할 수 있습니다. 현재 참여 체인: | 경로 | 예시 체인 | 메커니즘 | 수수료 | | :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------- | :------------------ | | **OFT 메시** | Arbitrum, Bera, Conflux, Ethereum, Flare, Hedera, Hyperliquid, Ink, Mantle, MegaETH, Monad, Morph, MP1, Optimism, Plasma, Polygon, Rootstock, Sei, Tempo, Unichain, X Layer | 소스에서 소각, Stable에서 발행 | 소스 체인 가스 비용만 | | **레거시 메시** | Tron, TON | 네이티브 USDT 잠금 → Arbitrum 허브 → Stable에서 USDT0 발행 | 0.03% + 소스 체인 가스 비용 | Ethereum과 Arbitrum은 두 경로를 모두 지원합니다. 네이티브 USDT를 보유한 사용자는 레거시 메시를 사용할 수 있고, USDT0를 보유한 사용자는 OFT 메시를 직접 사용할 수 있습니다. *** ### 컨트랙트 주소 | | 테스트넷 (chain ID 2201) | 메인넷 (chain ID 988) | | :---------------------------- | :------------------------------------------- | :------------------------------------------------------------------------------------------------------ | | **LayerZero EID** | `40374` | [LayerZero 배포된 컨트랙트](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable) 참조 | | **LayerZero Endpoint V2** | `0x3aCAAf60502791D199a5a5F0B173D78229eBFe32` | LayerZero 문서 참조 | | **USDT0 토큰** | `0x78Cf24370174180738C5B8E352B6D14c83a6c9A9` | `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` | | **USDT0 OApp (Stable에서)** | N/A | `0xedaba024be4d87974d5aB11C6Dd586963CcCB027` | | **소스 USDT0 (Sepolia)** | `0xc4DCC311c028e341fd8602D8eB89c5de94625927` | 소스 체인에서 메인넷 USDT0 사용 | | **소스 OApp (Sepolia)** | `0xc099cD946d5efCC35A99D64E808c1430cEf08126` | 소스 체인에서 메인넷 OApp 사용 | | **LiFi Diamond (Stable 메인넷)** | N/A | `0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37` | 전체 테스트넷 컨트랙트 목록(LayerZero 엔드포인트, DVN, executor)은 [테스트넷 에코시스템 컨트랙트](/ko/reference/testnet-ecosystem)를 참조하세요. *** ### STABLE OFT 컨트랙트 STABLE 토큰은 LayerZero OFT 표준을 사용하여 다른 체인으로 브리징됩니다. Stable의 어댑터는 아웃바운드 전송을 위해 STABLE을 잠그고, 각 원격 체인의 업그레이드 가능한 프록시는 래핑된 공급량을 발행하고 소각합니다. 보안 모델과 각 컨트랙트의 역할에 대한 설명은 [브리지 보안 및 DVN](/ko/explanation/bridge-security)을 참조하세요. | 체인 | 컨트랙트 | 주소 | | :----------- | :----------------------------- | :------------------------------------------- | | **Stable** | `StableOFTAdapter` | `0x386f92606b2D5E0A992ECc3704c31eF39Ff56392` | | **BSC** | `StableOFTUpgradeable` (proxy) | `0x011EBe7d75E2C9D1E0bD0be0bEf5C36f0A90075F` | | **HyperEVM** | `StableOFTUpgradeable` (proxy) | `0xa51dC81944a15623874981181a99D6c56B20ED56` | :::note 원격 체인 주소는 둘 다 EVM 호환임에도 불구하고 다릅니다. HyperCore에서 대상 주소를 활성화하려면 트랜잭션이 필요하며, 이는 배포 nonce를 증가시켜 BSC와는 다른 결정론적 주소를 생성합니다. 표준 설정은 [stablelabs/chain-oft](https://github.com/stablelabs/chain-oft)를 참조하세요. ::: *** ### Stable의 DVN 운영자 Stable의 브리지는 3/3 필수 DVN 구성으로 실행됩니다. 세 개의 독립적인 운영자가 각각 모든 크로스체인 메시지를 서명한 후에야 메시지가 수락됩니다. 선택적 풀은 없습니다. 세 개의 필수 서명자와 해당 DVN 컨트랙트 주소: | 운영자 | DVN 주소 | | :----------------- | :------------------------------------------- | | **LayerZero Labs** | `0x9c061c9a4782294eef65ef28cb88233a987f4bdd` | | **Canary** | `0x8d6cc20d84fbeb5733c60436ceb8957da2ac02c8` | | **Horizen** | `0x965a80dc87cec5848310e612dead84b543aef874` | 경로별 온체인 설정은 [LayerZero 배포된 컨트랙트](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable)를 참조하세요. 보안 근거는 [브리지 보안 및 DVN](/ko/explanation/bridge-security)을 참조하세요. *** ### 브리지 제공자 | 제공자 | 유형 | 상태 | 설명 | 문서 | | :------------------------------------------------------------------ | :---------------- | :------ | :------------------------------------- | :--------------------------------------------------------------------------- | | **[LayerZero](https://docs.layerzero.network/v2)** | 크로스체인 메시징 (OFT) | 운영 중 | USDT0 OFT 소각/발행 전송 지원; 이중 DVN 검증 | [docs.layerzero.network/v2](https://docs.layerzero.network/v2) | | **[Stargate](https://docs.stargate.finance/introduction/overview)** | 직접 브리지 (유동성 풀) | 운영 중 | 통합 유동성 풀; 스테이블코인 최적화 라우팅 | [docs.stargate.finance](https://docs.stargate.finance/introduction/overview) | | **[Gas.Zip](https://dev.gas.zip/overview)** | 직접 브리지 (유동성 라우팅) | 운영 중 | 350개 이상 체인에 걸친 유동성 라우팅; 빠른 완결성 | [dev.gas.zip](https://dev.gas.zip/overview) | | **[LiFi](https://docs.li.fi/api-reference/introduction)** | 브리지 애그리게이터 | 운영 중 | 여러 브리지와 DEX 스왑에 걸쳐 라우팅; SDK + REST API | [docs.li.fi](https://docs.li.fi/api-reference/introduction) | | **[Polymer](https://docs.polymerlabs.org/docs/build/start/)** | 크로스체인 상호운용성 (IBC) | 통합 진행 중 | Ethereum 네이티브 체인을 위한 IBC 기반 메시징 | [docs.polymerlabs.org](https://docs.polymerlabs.org/docs/build/start/) | | **[Relay](https://docs.relay.link/what-is-relay)** | 인텐트 기반 브리지 | 통합 진행 중 | 솔버 네트워크를 통한 가스리스 실행 | [docs.relay.link](https://docs.relay.link/what-is-relay) | #### LayerZero 이중 DVN 검증을 통해 USDT0 OFT 소각/발행 전송을 지원하는 크로스체인 메시징 프로토콜입니다. **기능** * OFT 표준 소스 소각, 대상 발행 전송 * 이중 DVN(분산 검증자 네트워크) 메시지 검증 * OFT 메시와 레거시 메시 경로 모두 지원 #### Stargate 스테이블코인 라우팅에 최적화된 유동성 풀 기반 브리지입니다. **기능** * 체인 전반의 통합 유동성 풀 * 스테이블코인 최적화 라우팅 * 즉각적이고 보장된 완결성 #### Gas.Zip 350개 이상의 체인에 걸쳐 빠른 전송을 지원하는 유동성 라우팅 프로토콜입니다. **기능** * 크로스체인 유동성 라우팅 * 빠른 완결성 * 넓은 체인 커버리지(350개 이상 체인) #### LiFi 여러 브리지와 DEX 스왑에 걸쳐 전송을 라우팅하는 브리지 애그리게이터입니다. **기능** * 멀티 브리지 경로 최적화 * SDK 및 REST API 통합 * DEX 스왑 애그리게이션 #### Polymer Ethereum 네이티브 체인을 위한 IBC 기반 크로스체인 메시징입니다. 통합 진행 중입니다. **기능** * Ethereum에서의 IBC 프로토콜 메시징 * 외부 검증자 없는 네이티브 상호운용성 #### Relay 솔버 네트워크를 통한 가스리스 실행을 제공하는 인텐트 기반 브리지입니다. 통합 진행 중입니다. **기능** * 인텐트 기반 브리징 * 사용자를 위한 가스리스 실행 * 솔버 네트워크 정산 *** ### 수수료 구조 | 제공자 | 수수료 모델 | | :--------------------- | :---------------------------------------------------------------------------------- | | **LayerZero (OFT 메시)** | 소스 체인 가스 비용만 (프로토콜 수수료 없음) | | **LayerZero (레거시 메시)** | 전송 금액의 0.03% (USDT0 팀이 부과) + 소스 체인 가스 비용 | | **Stargate** | 유동성 풀 수수료 적용; [Stargate 문서](https://docs.stargate.finance/introduction/overview) 참조 | | **LiFi** | 경로에 따라 애그리게이터 라우팅 수수료가 적용될 수 있음 | | **Gas.Zip** | 현재 수수료 일정은 [Gas.Zip 문서](https://dev.gas.zip/overview) 참조 | | **Relay** | 솔버 수수료; [Relay 문서](https://docs.relay.link/what-is-relay) 참조 | *** Stable을 통합하는 브리지가 있으신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의하세요. ## 연결 이 페이지는 Stable에 연결하는 데 필요한 네트워크 세부 정보를 통합해 제공합니다. ### 메인넷 | **필드** | **값** | | :----------- | :----------------------------------------------- | | 네트워크 이름 | Stable Mainnet | | Chain ID | `988` | | 통화 기호 | USDT0 | | EVM JSON-RPC | `https://rpc.stable.xyz` | | WebSocket | `wss://rpc.stable.xyz` | | 블록 탐색기 | [https://stablescan.xyz](https://stablescan.xyz) | ### 테스트넷 | **필드** | **값** | | :----------- | :--------------------------------------------------------------- | | 네트워크 이름 | Stable Testnet | | Chain ID | `2201` | | 통화 기호 | USDT0 | | EVM JSON-RPC | `https://rpc.testnet.stable.xyz` | | WebSocket | `wss://rpc.testnet.stable.xyz` | | 블록 탐색기 | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) | 서드파티 RPC 제공자에 대해서는 [RPC 제공자](/ko/reference/rpc-providers)를 참고하세요. 이러한 엔드포인트를 자동으로 연결해 주는 타입 지정 클라이언트는 [Stable SDK](/ko/explanation/sdk-overview)를 참고하세요. ### 속도 제한 공개 RPC 엔드포인트(`https://rpc.stable.xyz` 및 `https://rpc.testnet.stable.xyz`)는 **IP당 10초마다 1,000건의 요청**으로 속도가 제한됩니다. 제한을 초과한 요청은 `HTTP 429`를 반환합니다. 더 높은 처리량이 필요하면 [서드파티 RPC 제공자](/ko/reference/rpc-providers)를 사용하세요. :::note USDT0은 네이티브 가스 토큰으로는 **18자리 소수**(`address(x).balance`로 반환됨)를, ERC-20 토큰으로는 **6자리 소수**(`USDT0.balanceOf(x)`로 반환됨)를 사용합니다. 두 인터페이스 모두 동일한 기본 잔액을 기반으로 동작합니다. viem이나 ethers.js 같은 라이브러리는 네이티브 가스 토큰을 읽기 때문에 18자리 소수를 보고합니다. 정밀도 차이가 어떻게 조정되는지에 대한 자세한 내용은 [Stable에서의 USDT0 동작](/ko/explanation/usdt0-behavior)을 참고하세요. ::: ### 지갑에 Stable 추가하기 Stable을 수동으로 추가하려면 브라우저 지갑의 네트워크 설정을 열고 위 표의 값을 입력하세요. 필요한 필드는 다음과 같습니다: * **네트워크 이름** * **RPC URL** (EVM JSON-RPC 엔드포인트) * **Chain ID** * **통화 기호**: `USDT0` ### 연결 확인 체인 ID를 조회하여 RPC 엔드포인트에 접근 가능한지 확인하세요: ```bash cast chain-id --rpc-url https://rpc.stable.xyz ``` 예상 출력: ```text 988 ``` 테스트넷의 경우: ```bash cast chain-id --rpc-url https://rpc.testnet.stable.xyz ``` 예상 출력: ```text 2201 ``` ### 다음 추천 * [**빠른 시작**](/ko/tutorial/quick-start) — 5분 안에 첫 테스트넷 트랜잭션을 전송하세요. * [**테스트넷 USDT0 받기**](/ko/how-to/use-faucet) — 포셋에서 지갑에 자금을 채우거나 Sepolia에서 브리지하세요. * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 잔액을 다루는 코드를 작성하기 전에 18/6 소수 자릿수의 이중 역할을 이해하세요. ## 수탁(Custody) ### 수탁 개요 표 | **제공업체** | **카테고리** | **문서 / 시작하기** | **비고** | | :---------------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------- | :--------------------------- | | [Paxos](https://paxos.com/) | MPC 수탁 인프라 | [https://docs.paxos.com/guides/developer/account](https://docs.paxos.com/guides/developer/account) | Mastercard와 PayPal이 신뢰하는 서비스 | | [Fireblocks](https://www.fireblocks.com/) | MPC 수탁 인프라 | [https://developers.fireblocks.com/docs/quickstart](https://developers.fireblocks.com/docs/quickstart) | 트레저리 및 정산 워크플로우 | | [Fordefi](https://www.fordefi.com/) | MPC 수탁 인프라 | [https://docs.fordefi.com/](https://docs.fordefi.com/) | 정책 엔진 및 개발자 API | | [Anchorage](https://www.anchorage.com/) | 규제 준수 기관 수탁 | [https://www.anchorage.com/get-in-touch](https://www.anchorage.com/get-in-touch) | 연방 인가 은행; 450억 달러 이상 수탁 | ### 카테고리 안내 * **MPC 수탁 인프라:** 다자간 연산(multi-party computation)을 활용하여 여러 당사자에게 개인 키 제어 권한을 분산하는 플랫폼입니다. 이러한 플랫폼은 기관용 디지털 자산 운영을 위한 안전한 키 관리, 정책 엔진, 개발자 API를 제공합니다. * **규제 준수 기관 수탁:** 직접적인 규제 감독을 받는 인가된 수탁기관이 필요한 기관을 위한 연방 규제 은행급 수탁 서비스입니다. ### MPC 수탁 인프라 #### [Paxos](https://paxos.com/) Mastercard와 PayPal을 포함한 글로벌 기업들이 신뢰하는 규제 준수 블록체인 및 토큰화 인프라 플랫폼입니다. **기능** * 규제 준수 수탁 및 정산 인프라 * 기업급 자산 보관 * 기관용 토큰화 서비스 * 스테이블코인 운영을 위한 컴플라이언스 프레임워크 **시작하기**: 개발자 계정을 생성하고 [Paxos 개발자 온보딩 가이드](https://docs.paxos.com/guides/developer/account)를 따라 Stable 자산에 대한 수탁 및 정산을 설정하세요. #### [Fireblocks](https://www.fireblocks.com/) 전 세계 기관을 위한 수탁, 트레저리, 디지털 자산 운영을 지원하는 금융 인프라입니다. **기능** * MPC 기반 디지털 자산 수탁 * 안전한 전송 및 트레저리 워크플로우 * 기관용 정산 네트워크 * 스테이블코인 프로그램 인프라 **시작하기**: [Fireblocks 퀵스타트 가이드](https://developers.fireblocks.com/docs/quickstart)를 따라 워크스페이스를 설정하고, Stable을 지원 네트워크로 구성한 뒤 디지털 자산 관리를 시작하세요. #### [Fordefi](https://www.fordefi.com/) 탈중앙화 금융을 위해 구축된 기관용 MPC 지갑 및 보안 플랫폼입니다. Fordefi는 Web3 기관을 위한 키 관리, 정책 제어, 개발자 API를 제공합니다. **기능** * MPC 기반 분산 키 생성 및 임계값 서명 * 기관용 정책 엔진 및 승인 워크플로우 * 프로그래밍 방식 지갑 운영을 위한 개발자 API * 브라우저 확장 프로그램, 모바일, API 인터페이스 **시작하기**: [Fordefi 개발자 문서](https://docs.fordefi.com/)를 검토하여 볼트를 생성하고, 승인 정책을 구성한 뒤 API를 통해 Stable에 연결하세요. ### 규제 준수 기관 수탁 #### [Anchorage](https://www.anchorage.com/) 450억 달러 이상의 디지털 자산을 안전하고 규제 준수하여 수탁하는 연방 인가 국립 은행입니다. **기능** * 은행급 디지털 자산 수탁 * 기업용 접근 제어 * 규제 준수 기관 운영 * 감사 가능하고 컴플라이언스를 준수하는 자산 보관 **시작하기**: [기관 온보딩 페이지](https://www.anchorage.com/get-in-touch)를 통해 Anchorage에 문의하여 Stable 자산의 규제 준수 수탁을 위한 계정 설정 절차를 시작하세요. *** Stable과 통합되는 수탁 인프라를 보유하고 계신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의해 주세요. ## 개발자 지원 ### FAQ 다음과 같은 주제를 다루는 개발자 중심 질문 모음집입니다: * Stable 네트워크에 어떻게 연결하나요? * 일반적인 EVM 도구와 호환되는 표준 JSON-RPC 요청을 사용하여 네트워크와 상호작용할 수 있습니다. * 트랜잭션 수수료에는 어떤 통화가 사용되나요? * 트랜잭션 수수료는 USDT0로 지불됩니다. 표준 기본 가스 가격 외에 추가적인 수수료 매개변수는 필요하지 않습니다. * 업데이트는 어디서 추적할 수 있나요? * 모든 프로토콜 및 개발자 대상 변경사항은 릴리스 & 변경 로그에서 전달됩니다. * Stable은 계정 추상화를 지원하나요? * 네. EIP-7702는 EOA가 일시적으로 스마트 계정 동작으로 작동할 수 있게 합니다. * 자세한 내용은 [여기](ko/architecture/usdt-specific-features/eip-7702-and-aa)에서 읽을 수 있습니다. * 트랜잭션 결과는 어디서 볼 수 있나요? * 블록에 포함되면 다음을 통해 결과를 볼 수 있습니다: * 잔액 읽기 * 컨트랙트 상태 조회 * 로그 및 방출된 이벤트 * Stable용 스마트 컨트랙트는 어떻게 빌드하나요? * 다음과 같은 표준 EVM 개발자 워크플로를 사용할 수 있습니다: * Solidity 기반 컨트랙트 * 네트워크와 상호작용하기 위한 JSON-RPC 라이브러리 이 페이지는 공개 테스트넷 사용 중 일반적인 질문이 발생하면서 확장될 것입니다. ### 지원 채널 개발자는 기술 지원을 위해 Stable 팀과 직접 소통할 수 있습니다. * **Discord**: [https://discord.gg/stablexyz](https://discord.gg/stablexyz) 개발자 채널에 참여하세요. * **이슈 보고**: 공개 깃헙 레포가 열리면 지침이 제공될 예정입니다. 커뮤니티 플랫폼이 사용 가능해지면 지원 연락처가 업데이트될 예정입니다. ## DEX 현물 거래, 유동성 공급 및 온체인 라우팅을 위한 Stable의 DEX 배포입니다. Stable은 [공식 Uniswap v3 배포 목록](https://gov.uniswap.org/t/official-uniswap-v3-deployments-list/24323/13#p-58106-stable-4)에 등재되어 있습니다. Stable의 Uniswap v3 컨트랙트는 거버넌스에서 표준으로 인정받았으며 기본 프론트엔드인 [Stable Swap](https://swap.stable.xyz)을 통해 라우팅됩니다. ### 개요 표 | **제공자** | **카테고리** | **상태** | **문서 / 시작하기** | **참고** | | :---------------------------------------- | :--------- | :-------------- | :----------------------------------------------------------------- | :------------------------------------------------------------------------- | | [**Uniswap v3**](https://swap.stable.xyz) | 집중 유동성 AMM | 표준 (메인넷에서 운영 중) | [docs.uniswap.org](https://docs.uniswap.org/contracts/v3/overview) | 2026년 5월 12일 공식 Uniswap v3 배포 목록에 등재됨. 프론트엔드: Stable Swap. 배포자: Protofire. | ### Uniswap v3 집중 유동성 풀과 표준 수수료 등급을 갖춘 Stable의 표준 Uniswap v3 배포입니다. Stable Swap은 활발하게 유지 관리되는 기본 프론트엔드이며, 거래는 아래의 컨트랙트를 통해 라우팅됩니다. 크로스체인 유동성은 LayerZero를 통해 유입됩니다. **기능** * v3 포지션 NFT를 사용하는 집중 유동성 AMM * 표준 `SwapRouter02`, `Quoter V2` 및 `Universal Router` 통합 경로 * 가스 없는 승인을 위한 `Permit2` 지원 * 레거시 라우팅을 위한 v2 스타일 정량곱 풀도 배포됨 #### 메인넷 컨트랙트 주소 출처: [RFC: Stable Application for Canonical Uniswap v3 Deployment](https://gov.uniswap.org/t/rfc-stable-application-for-canonical-uniswap-v3-deployment/26080) 및 [공식 Uniswap v3 배포 목록](https://gov.uniswap.org/t/official-uniswap-v3-deployments-list/24323/13#p-58106-stable-4). | **컨트랙트** | **주소** | | :----------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | | **v3 Core Factory** | [0x88F0a512eF09175D456bc9547f914f48C013E4aA](https://stablescan.xyz/address/0x88F0a512eF09175D456bc9547f914f48C013E4aA) | | **Universal Router** | [0x5Be52b52f3d1dbC324d2959637471a4208626144](https://stablescan.xyz/address/0x5Be52b52f3d1dbC324d2959637471a4208626144) | | **Swap Router02** | [0x32eaf9B5d5F2CD7361c5012890C943D7de84C22a](https://stablescan.xyz/address/0x32eaf9B5d5F2CD7361c5012890C943D7de84C22a) | | **Quoter V2** | [0xb070179E7032CdA868b53e6C1742F80c9e940d1A](https://stablescan.xyz/address/0xb070179E7032CdA868b53e6C1742F80c9e940d1A) | | **Nonfungible Token Position Manager** | [0x3BdC3437405f7D801b6036532713fc1F179136a6](https://stablescan.xyz/address/0x3BdC3437405f7D801b6036532713fc1F179136a6) | | **Nonfungible Token Position Descriptor V1.3.0** | [0x7Cf5987951E48ADf235cc9194bCdc708Eb692D82](https://stablescan.xyz/address/0x7Cf5987951E48ADf235cc9194bCdc708Eb692D82) | | **NFT Descriptor Library V1.3.0** | [0xF7815833076D83161414A46c4E993dC8f22A7ADd](https://stablescan.xyz/address/0xF7815833076D83161414A46c4E993dC8f22A7ADd) | | **Descriptor Proxy** | [0xcd2cD0E139eC5581138E18C6DBB189c53efBAE95](https://stablescan.xyz/address/0xcd2cD0E139eC5581138E18C6DBB189c53efBAE95) | | **Proxy Admin** | [0x51D1E70B8cAbDF4F3aB056475802AB1687b3EA23](https://stablescan.xyz/address/0x51D1E70B8cAbDF4F3aB056475802AB1687b3EA23) | | **Tick Lens** | [0x8dF0D1614aae99352045c62d24d54E72b38111ec](https://stablescan.xyz/address/0x8dF0D1614aae99352045c62d24d54E72b38111ec) | | **v3 Migrator** | [0x2C5f4275F1a278BF328D56CB9db304e915DE3082](https://stablescan.xyz/address/0x2C5f4275F1a278BF328D56CB9db304e915DE3082) | | **v3 Staker** | [0xA32e3E127FF46db40ab3c4775be97ED760AD7178](https://stablescan.xyz/address/0xA32e3E127FF46db40ab3c4775be97ED760AD7178) | | **Permit2** | [0x000000000022D473030F116dDEE9F6B43aC78BA3](https://stablescan.xyz/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | | **Multicall 2** | [0x208099D6E8a107aD485CD1374A6EC5Abd98c7F11](https://stablescan.xyz/address/0x208099D6E8a107aD485CD1374A6EC5Abd98c7F11) | | **V2 Core Factory** | [0x25D2d657F539F2bB16eC82773cBE5ee49ddD3c69](https://stablescan.xyz/address/0x25D2d657F539F2bB16eC82773cBE5ee49ddD3c69) | | **Uniswap V2 Router02** | [0xa571dc7c4f2369F1cA24D3a7E8a35c07Ff52bfC0](https://stablescan.xyz/address/0xa571dc7c4f2369F1cA24D3a7E8a35c07Ff52bfC0) | :::note Stable은 2026년 4월 UAC 거버넌스 절차를 완료한 후 2026년 5월 12일부로 Uniswap v3 배포 목록에 인정되었습니다. 이 배포는 Protofire가 유지 관리하며 LayerZero를 통한 브리지 연결을 제공합니다. ::: #### 스왑 견적 받기 `Quoter V2`는 거래를 실행하지 않고도 주어진 입력에 대한 예상 출력을 반환합니다. Stable의 RPC를 가리키는 모든 EVM 도구에서 사용할 수 있습니다. ```bash cast call 0xb070179E7032CdA868b53e6C1742F80c9e940d1A \ "quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256)" \ "(,,,,0)" \ --rpc-url https://rpc.stable.xyz ``` ```text (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) ``` ``, ``, `` 및 ``(`100`, `500`, `3000`, `10000` 중 하나)를 견적을 받으려는 풀의 값으로 바꾸세요. 애플리케이션 통합의 경우, 위 주소를 가리키는 [Uniswap v3 SDK](https://docs.uniswap.org/sdk/v3/overview) 또는 [Universal Router](https://docs.uniswap.org/contracts/universal-router/overview)를 사용하는 것이 좋습니다. *** ### Stable을 통합하는 DEX가 있으신가요? 이 페이지에 등재되려면 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 팀에 문의하세요. ### 다음 추천 항목 * [Stable에 연결](/ko/reference/connect): 메인넷 및 테스트넷의 체인 ID, RPC 엔드포인트 및 블록 익스플로러. * [브리지](/ko/reference/bridges): USDT0 및 기타 자산을 Stable로 이동하여 유동성을 공급하거나 거래를 라우팅하세요. * [오라클](/ko/reference/oracles): 가격 책정 및 청산을 위해 스왑 견적과 함께 사용할 수 있는 가격 피드. ## Distribution precompile 참조 :::note **개념:** distribution 모듈이 무엇을 하는지와 언제 사용하는지에 대해서는 [Distribution 모듈](/ko/explanation/distribution-module)을 참조하세요. ::: ### 개요 `distribution` precompiled 컨트랙트는 EVM 환경에서 Stable SDK의 `x/distribution` 모듈 기능을 사용할 수 있게 해주는 브릿지 역할을 합니다. ### 목차 1. **[개념](#concepts)** 2. **[구성](#configuration)** 3. **[메서드](#methods)** 4. **[이벤트](#events)** ### 개념 `distribution` precompiled 컨트랙트는 위임자 또는 예치자가 호출자인지 확인하기 위한 추가 검증을 수행합니다. ### 구성 컨트랙트 주소와 가스 비용은 미리 정의되어 있습니다. #### 컨트랙트 주소 * `0x0000000000000000000000000000000000000801` ### 메서드 #### `setWithdrawAddress` 위임자가 검증인에게 위임한 토큰에 대한 보상을 받을 주소를 설정합니다. 때때로 위임자가 자기 위임(self-delegated)인 경우, 검증인 주소가 위임자로 사용됩니다. `SetWithdrawAddress`는 인출자 주소가 성공적으로 설정되면 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ----------------- | ------- | ------------ | | delegatorAddress | address | 위임자의 주소 | | withdrawerAddress | address | 위임 보상을 받을 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ----------------------- | | success | bool | 인출자 주소가 성공적으로 설정되면 true | #### `withdrawDelegatorRewards` 검증인으로부터 위임자가 받을 보상을 인출합니다. 검증인이 위임자에게 보상하는 모든 종류의 토큰이 단일 트랜잭션으로 인출됩니다. `WithdrawDelegatorRewards`는 보상이 성공적으로 인출되면 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | delegatorAddress | address | 위임자의 주소 | | validatorAddress | address | 검증인의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------ | ------- | ------------------ | | amount | Coin\[] | 위임자가 받을 다양한 토큰의 보상 | `Coin`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------ | ------- | --------- | | denom | string | 보상의 denom | | amount | uint256 | 보상의 금액 | #### `withdrawValidatorCommission` 검증인의 수수료를 인출합니다. 검증인이 수수료로 받는 모든 종류의 토큰이 단일 트랜잭션으로 인출됩니다. `WithdrawValidatorCommission`은 수수료가 성공적으로 인출되면 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증인의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------ | ------- | ------------------- | | amount | Coin\[] | 검증인이 받을 다양한 토큰의 수수료 | #### `validatorDistributionInfo` 검증인이 받을 보상을 나타내는 distribution 정보를 반환합니다. 검증인은 자신의 주소에서 자신에게 토큰을 위임하여 위임자로 행동할 수 있는데, 이를 self-bonded라고 합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증인의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ---------------- | ------------------------- | -------------------- | | distributionInfo | ValidatorDistributionInfo | 검증인의 distribution 정보 | `ValidatorDistributionInfo`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------------- | ---------- | ------------------- | | operatorAddress | address | 검증인 운영자의 주소 | | selfBondRewards | DecCoin\[] | 검증인의 self-bonded 금액 | | commission | DecCoin\[] | 검증인의 수수료 | `DecCoin`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------- | ------- | --------- | | denom | string | 보상의 denom | | amount | uint256 | 보상의 금액 | | precision | uint8 | 보상의 정밀도 | #### `validatorOutstandingRewards` 검증인의 미결제 보상(outstanding rewards)을 반환합니다. 미결제 보상은 전체 보상 풀을 나타냅니다: 검증인의 수수료와 self-bonded 보상, 그리고 위임자에게 지급해야 할 총 보상입니다. 예를 들어, 검증인 A에 위임자 B, C, D가 있다면 미결제 보상은 A의 수수료와 self-bonded 보상에 B, C, D의 보상을 더한 것과 같습니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증인의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---------- | ----------- | | rewards | DecCoin\[] | 검증인의 미결제 보상 | #### `validatorCommission` 검증인의 수수료를 반환합니다. 이 메서드는 `withdrawValidatorCommission` 메서드를 호출하기 전에 검증인의 수수료를 조회하는 데 사용됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증인의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ---------- | ---------- | -------- | | commission | DecCoin\[] | 검증인의 수수료 | #### `validatorSlashes` 시작 높이와 종료 높이 사이의 검증인 슬래싱 내역을 반환합니다. 슬래싱은 검증인이 악의적으로 행동하거나 이중 서명, 부정 행위, 체인 규칙 미준수 등 네트워크 규칙을 위반할 때 부과되는 벌금입니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | --------- | | validatorAddress | address | 검증인의 주소 | | startingHeight | uint64 | 시작 높이 | | endingHeight | uint64 | 종료 높이 | | pageRequest | PageReq | 페이지네이션 요청 | `PageReq`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ---------- | ------ | ------------------ | | key | bytes | 페이지네이션의 키 | | offset | uint64 | 페이지네이션의 오프셋 | | limit | uint64 | 페이지네이션의 한도 | | countTotal | bool | 전체 페이지 수를 셀지 여부 | | reverse | bool | 페이지네이션을 역순으로 할지 여부 | ##### 출력 | 이름 | 타입 | 설명 | | ---------- | ---------------------- | --------- | | slashes | ValidatorSlashEvent\[] | 검증인의 슬래싱 | | pagination | PageResp | 페이지네이션 응답 | `ValidatorSlashEvent`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------------- | ------ | ------- | | validatorPeriod | uint64 | 검증인의 기간 | | fraction | Dec | 슬래싱의 비율 | `Dec`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------- | ------ | -------- | | value | uint64 | Dec의 값 | | precision | uint8 | Dec의 정밀도 | `PageResp`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------- | ------ | ------------ | | nextKey | bytes | 페이지네이션의 다음 키 | | total | uint64 | 전체 페이지 수 | #### `delegationRewards` 위임자가 검증인으로부터 받는 보상을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | | validatorAddress | address | 검증인의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---------- | ------------------ | | rewards | DecCoin\[] | 위임자가 검증인으로부터 받는 보상 | #### `delegationTotalRewards` 위임자가 모든 검증인으로부터 받는 총 보상을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---------------------------- | ----------------------- | | rewards | DelegationDelegatorReward\[] | 위임자가 모든 검증인으로부터 받는 총 보상 | | total | DecCoin\[] | 보상의 총 금액 | `DelegationDelegatorReward`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ---------------- | ---------- | ------------------ | | validatorAddress | address | 검증인의 주소 | | reward | DecCoin\[] | 위임자가 검증인으로부터 받는 보상 | #### `delegatorValidators` 위임자가 본딩된 검증인들을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ---------- | --------- | ------------- | | validators | string\[] | 위임자가 본딩된 검증인들 | #### `delegatorWithdrawAddress` `setWithdrawAddress` 메서드로 설정한 위임 보상을 받을 주소를 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | ##### 출력 | 이름 | 타입 | 설명 | | --------------- | ------- | ------------ | | withdrawAddress | address | 위임 보상을 받을 주소 | ### 이벤트 #### SetWithdrawAddress | 이름 | 타입 | 인덱싱됨 | 설명 | | --------------- | ------- | ---- | ------------ | | caller | address | Y | 호출자(위임자)의 주소 | | withdrawAddress | address | N | 위임 보상을 받을 주소 | #### WithdrawDelegatorRewards | 이름 | 타입 | 인덱싱됨 | 설명 | | ---------------- | ------- | ---- | ------- | | delegatorAddress | address | Y | 위임자의 주소 | | validatorAddress | address | Y | 검증인의 주소 | | amount | uint256 | N | 보상의 금액 | #### WithdrawValidatorCommission | 이름 | 타입 | 인덱싱됨 | 설명 | | ---------------- | ------- | ---- | --------- | | validatorAddress | address | Y | 검증인의 주소 | | commission | uint256 | N | 수수료의 총 금액 | ## EIP-7702 Stable은 **EIP-7702**를 지원하며, 이를 통해 EOA가 자신의 계정 코드를 기존 스마트 컨트랙트로 설정할 수 있습니다. EOA는 원래의 주소와 프라이빗 키를 유지하면서 위임 대상의 로직을 실행합니다. :::note **개념:** EIP-7702가 Stable에서 가능하게 하는 것, 위임 모델, 보안 고려사항에 대해서는 [EIP-7702](/ko/explanation/eip-7702)를 참조하세요. 전체 명세는 [EIP-7702 spec](https://eips.ethereum.org/EIPS/eip-7702)을 참조하세요. ::: ### 트랜잭션 형식 EIP-7702는 `authorizationList` 필드를 포함하는 트랜잭션 타입 `0x04`를 사용합니다. 각 authorization은 해당 트랜잭션에서 EOA가 실행할 코드를 가진 위임 컨트랙트를 지정합니다. ```typescript { type: 4, to: eoa.address, data: delegateCallData, authorizationList: [signedAuthorization], maxPriorityFeePerGas: 0n, // always 0 on Stable // ... standard EIP-1559 fields } ``` authorization이 담는 정보: * `chainId`: 대상 체인과 일치해야 합니다. * `address`: 위임 컨트랙트 주소입니다. * `nonce`: authorization nonce(트랜잭션 nonce와 별개)입니다. EIP-7702를 지원하는 지갑과 라이브러리는 authorization 형식을 자동으로 처리합니다. ### 도구 * **ethers.js**: `wallet.signAuthorization({ chainId, address, nonce })`는 `authorizationList`에 포함할 서명된 authorization을 생성합니다. * **viem**: walletClient와 함께 `signAuthorization`을 사용한 다음, 그 결과를 `sendTransaction`에 전달합니다. * **Hardhat / Foundry**: 도구 체인 버전이 Pectra 하드포크를 지원하면 표준 EIP-7702 트랜잭션 형식이 동작합니다. ### 다음 권장 사항 * [**EIP-7702 개념**](/ko/explanation/eip-7702) — 위임 모델과 사용 시점을 이해하세요. * [**계정 추상화 (EIP-7702)**](/ko/reference/eip-7702-api) — 일괄 결제, 지출 한도, 세션 키를 단계별로 구현하세요. ## FAQ ### 일반 정보 **Stable이란 무엇인가요?** Stable은 USDT를 위한 전용 네트워크로 설계된 **고성능 블록체인**으로, USDT의 글로벌 전송 방식을 혁신하는 것을 목표로 합니다. **Stable은 다른 블록체인과 무엇이 다른가요?** Stable은 USDT에 최적화된 고성능 네트워크입니다. USDT를 기본 가스 토큰으로 사용하고, 보장된 블록스페이스, USDT0 전송 집계 기능 등을 제공하며, 모두 높은 확장성의 아키텍처를 기반으로 구축되어 있습니다. ### 기술 특징 **Stable은 확장성을 어떻게 향상시키나요?** Stable은 트랜잭션 라이프사이클 내 모든 단계 - State DB, 실행 엔진, 합의, USDT 전용 최적화 - 를 업그레이드하는 풀스택 접근 방식을 사용합니다. **Stable이 향후 DAG 기반 합의로 업그레이드될 수 있나요?** 네. StableBFT와 호환되지 않는 Narwhal 및 Tusk와 달리, Autobahn은 DAG 기반 PBFT 아키텍처를 제공하며, Stable의 합의 계층과 자연스럽게 통합될 수 있습니다. **Stable은 EVM 호환되나요? 기존 dApp을 이식할 수 있나요?** 네. Stable은 완전한 EVM 호환성을 가지며, 사용자 및 개발자는 기존 Ethereum 스마트 컨트랙트, 툴, 지갑 등을 그대로 사용할 수 있습니다. ### USDT 관련 기능 **Stable에서 USDT0를 어떻게 받을 수 있나요?** USDT0는 OFT 표준을 따르기 때문에, LayerZero 브릿지를 통해 다른 네트워크에서 쉽게 Stable로 USDT0를 옮길 수 있습니다. **Stable의 기타 USDT 전용 기능은 무엇이 있나요?** 다음과 같은 기능들이 추가될 예정입니다. * 보장된 블록스페이스: 기관 사용자가 네트워크 혼잡 여부와 무관하게 예측 가능한 레이턴시와 비용으로 블록 공간을 확보할 수 있도록 합니다 * USDT 전송 집계: 여러 개의 USDT0 전송을 묶어서 처리하여, 처리량을 향상하고 오버헤드를 감소시킵니다. * 기밀 전송: 거래 금액에 대한 프라이버시를 보호하면서도 규제를 준수할 수 있습니다. **Stable Pay이란 무엇인가요?** Stable Pay은 초보자와 고급 사용자 모두를 위한 간단하고 직관적인 탈중앙화 지갑입니다. 소셜 로그인 등으로 쉽게 온보딩할 수 있으며, 기존 지갑도 별도 마이그레이션 없이 연결 가능합니다. 웹과 모바일 모두에서 제공되어, 언제 어디서나 안전한 자산 접근이 가능합니다. ## 가스 가격 책정 참조 Stable에서의 트랜잭션 구성, 가스 추정, 도구 설정. :::note **개념:** Stable이 단일 구성 요소 수수료 모델을 사용하는 이유와 이더리움과의 비교에 대해서는 [가스 가격 책정](/ko/explanation/gas-pricing)을 참조하세요. ::: ### 트랜잭션 구성 Stable에서 트랜잭션을 구성할 때 `maxPriorityFeePerGas`를 `0`으로 설정하세요. 클라이언트는 가장 최근 블록에서 최신 기본 수수료(base fee)를 가져와야 하며, `maxFeePerGas`를 계산할 때 안전 마진을 포함해야 합니다. ```javascript // ethers.js v6 const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const maxPriorityFeePerGas = 0n; // always 0 on Stable const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // double the base fee as safety margin const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: parseEther("0.01"), maxFeePerGas, maxPriorityFeePerGas, }); ``` ```text Native USDT0 transfer confirmed. Fee ≈ 0.0000021 USDT0 at baseFee = 1 gwei. ``` ### 가스 추정 이더리움에서와 마찬가지로 `eth_estimateGas`와 `eth_gasPrice`를 사용하세요. 주요 차이점은 `eth_maxPriorityFeePerGas`가 항상 `0`을 반환한다는 것입니다. ```javascript const gasPrice = await provider.send("eth_gasPrice", []); const gasEstimate = await provider.estimateGas({ to: contractAddress, data: callData, }); const estimatedFeeInUSDT0 = gasPrice * gasEstimate; ``` ### 도구 설정 * **Hardhat / Foundry**: 특별한 설정이 필요 없습니다. 표준 EVM 설정으로 작동합니다. 설정에서 우선순위 수수료를 명시적으로 지정하는 경우 `0`으로 설정하세요. * **지갑**: 우선순위 팁 입력 필드를 숨기거나 비활성화하세요. 이 값은 효과가 없으므로 표시하면 사용자에게 혼란을 줄 수 있습니다. * **모니터링**: 수수료 분석 대시보드는 우선순위 수수료를 추적하지 않아야 합니다. 항상 0이 됩니다. ### 다음 추천 * [**가스 가격 책정 개념**](/ko/explanation/gas-pricing) — Stable이 단일 구성 요소 수수료 모델을 사용하는 이유를 이해하세요. * [**이더리움 비교**](/ko/explanation/ethereum-comparison) — 이더리움에서 포팅할 때 마주치게 될 모든 동작 차이를 검토하세요. * [**JSON-RPC API**](/ko/reference/json-rpc-api) — Stable이 노출하는 `eth_*` 메서드를 참조하세요. ## 가스 면제 프로토콜 이 문서는 가스 면제 메커니즘을 설명합니다: 트랜잭션 형식, 마커 라우팅, 거버넌스 제어, 그리고 Waiver Server API. :::note **개념:** 가스 면제가 무엇이며 왜 존재하는지에 대해서는 [가스 면제](/ko/explanation/gas-waiver)를 참조하세요. 호스팅된 Waiver Server를 대상으로 한 통합 방법 가이드는 [가스 무료 트랜잭션 활성화](/ko/how-to/integrate-gas-waiver)를 참조하세요. ::: ### 개요 가스 면제는 거버넌스에서 승인된 소수의 주소 집합("waivers")이 `gasPrice = 0`으로 트랜잭션을 제출할 수 있도록 허용하여 Stable에서 가스 없는 최종 사용자 트랜잭션을 가능하게 합니다. Stable은 현재 프로토콜별 래퍼 로직을 구현하지 않고도 가스 없는 UX를 제공하기 위해 통합할 수 있는 waiver 서비스("Waiver Server")를 운영합니다. ### 범위 이 사양은 다음을 다룹니다: * 가스가 면제된 트랜잭션에 대한 프로토콜 수준 규칙 * 래퍼 트랜잭션 메커니즘과 마커 주소 * 거버넌스가 제어하는 권한 부여 및 허용된 대상 * 서명된 사용자 트랜잭션을 제출하기 위한 Waiver Server 인터페이스 ### 정의 * **Waiver**: 가스가 면제된 트랜잭션을 제출할 수 있도록 검증자 거버넌스를 통해 온체인에 등록된 이더리움 주소입니다. * **InnerTx**: `gasPrice = 0`으로 최종 사용자가 서명한 트랜잭션입니다. * **WrapperTx**: 사용자의 `InnerTx`를 체인으로 전송하고 실행을 승인하기 위해 waiver가 서명한 트랜잭션입니다. * **마커 주소**: waiver 래퍼 트랜잭션을 식별하는 데 사용되는 센티넬 주소입니다: `0x000000000000000000000000000000000000f333`. * **AllowedTarget**: waiver를 특정 컨트랙트 주소와 메서드 선택자로 제한하는 정책입니다. ### 살펴보기 가스 면제는 래퍼 트랜잭션 패턴을 사용합니다: 1. 사용자가 `gasPrice = 0`으로 `InnerTx`에 서명합니다. 2. waiver가 `InnerTx`를 `WrapperTx`로 감싸서 브로드캐스트합니다. 3. 검증자가 마커 트랜잭션을 감지하고, waiver 권한 부여 및 정책 제약 조건을 확인한 다음, 내장된 `InnerTx`를 실행합니다. Stable은 승인된 waiver로 온체인에 등록된 waiver 서비스(Waiver Server)를 운영합니다. 서명된 `InnerTx` 페이로드를 제출하기 위해 Waiver Server API와 통합합니다. ### 프로토콜 사양 #### 마커 주소 라우팅 트랜잭션은 다음의 경우에만 waiver 래퍼 트랜잭션으로 취급됩니다: * `to == 0x000000000000000000000000000000000000f333`. 프로토콜은 트랜잭션 `data` 필드를 인코딩된 내부 트랜잭션 페이로드로 해석하고 아래의 waiver 검증 규칙을 사용하여 처리합니다. #### 권한 부여 및 정책 검사 각 후보 래퍼 트랜잭션에 대해 검증자는 다음을 적용해야 합니다: 1. **Waiver 권한 부여** * `WrapperTx.from`은 거버넌스를 통해 온체인에 등록된 waiver 주소여야 합니다. 2. **가스 면제** * `WrapperTx.gasPrice`는 `0`이어야 합니다. * `InnerTx.gasPrice`는 `0`이어야 합니다. 3. **대상 허용 목록** * `InnerTx.to`와 `InnerTx.data`에서 추출한 메서드 선택자는 waiver의 `AllowedTarget` 정책에 의해 허용되어야 합니다. 4. **값 제한** * `WrapperTx.value`는 `0`이어야 합니다. 검사 중 하나라도 실패하면 검증자는 래퍼 트랜잭션을 거부하고 내부 트랜잭션을 실행하지 않습니다. #### 실행 의미론 모든 검사를 통과하면: 1. 프로토콜은 사용자의 `from`, `nonce`, 호출 의미론을 보존하면서 `InnerTx`를 사용자로서 실행합니다. 2. 가스 정산은 waiver 메커니즘에 의해 처리됩니다: 사용자는 가스를 지불하지 않으며, waiver 트랜잭션은 이 기능의 정의에 따라 `gasPrice = 0`을 사용합니다. 3. 래퍼 트랜잭션은 `InnerTx`의 실행(언래핑 및 검증에 대한 오버헤드 포함)을 처리하기에 충분한 `gasLimit`을 제공해야 합니다. ### 트랜잭션 형식 #### WrapperTx 래퍼 트랜잭션은 waiver가 서명하여 마커 주소로 전송됩니다. ```javascript WrapperTx { from: waiver_address, to: 0x000000000000000000000000000000000000f333, value: 0, // must be zero data: RLP(InnerTx), // RLP-encoded inner transaction gasPrice: 0, // must be zero gasLimit: sufficient_for_inner, // must cover inner execution + overhead nonce: waiver_nonce } ``` #### InnerTx 내부 트랜잭션은 최종 사용자가 서명합니다. ```javascript InnerTx { from: user_address, to: target_contract, value: value, data: call_data, gasPrice: 0, // must be zero gasLimit: execution_gas, nonce: user_nonce } ``` ### 거버넌스가 제어하는 접근 waiver 권한 부여는 검증자 거버넌스에 의해 온체인에서 관리됩니다. 거버넌스 제어는 다음을 제공합니다: * waiver 주소의 검토 가능한 권한 부여 * waiver 등록 및 업데이트의 온체인 투명성 * 취소 기능 * `AllowedTarget`을 통한 waiver별 범위 지정 ### 보안 모델 #### 최종 사용자 서명 무결성 사용자는 `InnerTx`에 서명합니다. waiver는 서명을 무효화하지 않고는 내부 트랜잭션 페이로드를 수정할 수 없습니다. 사용자가 의도한 트랜잭션 페이로드에만 서명하도록 여전히 보장해야 합니다. #### 신뢰 경계 가스 면제는 파트너가 Waiver Server를 통해 제출을 라우팅하는 경우 서비스 의존성을 도입합니다: * 서비스의 가용성은 가스 없는 트랜잭션을 제출하는 능력에 영향을 미칩니다. * 권한 부여는 온체인에 남아 있습니다. 등록된 waiver 주소만 유효한 래퍼 제출을 생성할 수 있습니다. ### 통합 다음과 같이 통합합니다: 1. 사용자로부터 서명된 `InnerTx`(`gasPrice = 0`)를 수집합니다. 2. 서명된 내부 트랜잭션을 Waiver Server API에 제출합니다. 3. 스트리밍된 결과를 처리하고 최종 사용자에게 트랜잭션 해시를 표시합니다. ### Waiver server #### 살펴보기 Waiver Server는 서명된 사용자 `InnerTx` 페이로드를 waiver가 승인한 래퍼 트랜잭션으로 감싸서 브로드캐스트합니다. 래퍼 트랜잭션을 구성하거나 waiver 주소를 운영할 필요가 없습니다. #### 엔드포인트 및 기본 URL 기본 URL: * 메인넷: TBD * 테스트넷: `https://waiver.testnet.stable.xyz` #### 인증 health를 제외한 모든 엔드포인트는 베어러 토큰 인증이 필요합니다: ``` Authorization: Bearer ``` #### API ##### GET `/v1/health` 상태 확인 엔드포인트. 인증: 없음. ##### POST `/v1/submit` 서명된 내부 트랜잭션 배치를 제출합니다. 인증: 필수 (`Bearer`). 요청 본문: ```json { "transactions": ["0x", "0x"] } ``` 응답은 NDJSON(줄바꿈으로 구분된 JSON)으로 스트리밍됩니다. 각 줄은 제출된 트랜잭션 인덱스에 해당합니다. 예시: ```json {"index":0,"id":"abc123","success":true,"txHash":"0x..."} {"index":1,"id":"def456","success":false,"error":{"code":"VALIDATION_FAILED","message":"invalid signature"}} ``` ##### GET `/v1/submit` 스트리밍 제출을 위한 WebSocket 인터페이스. 인증: 필수 (`Bearer`). #### 통합 예시 ```javascript const WAIVER_SERVER = "https://waiver.testnet.stable.xyz"; async function submitGaslessTransaction(signedInnerTxHex, apiKey) { const response = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex], }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); console.log(result); } } } ``` #### 사용자 InnerTx 생성 `gasPrice = 0`으로 `InnerTx`를 구성한 다음 사용자 서명을 수집할 책임이 있습니다. 예시: ```javascript import { ethers } from "ethers"; async function createInnerTx(userWallet, contractAddress, callData, nonce) { const innerTx = { to: contractAddress, data: callData, value: value, gasPrice: 0, // must be 0 for waiver gasLimit: 100000, nonce: nonce, chainId: 2201, // 988 for mainnet, 2201 for testnet }; return await userWallet.signTransaction(innerTx); } ``` #### 오류 코드 * `PARSE_ERROR`: 트랜잭션 파싱 실패 * `INVALID_REQUEST`: 잘못된 형식의 요청 본문 * `BATCH_SIZE_EXCEEDED`: 배치 크기가 허용된 최대값 초과 * `VALIDATION_FAILED`: 트랜잭션 검증 실패 * `BROADCAST_FAILED`: 체인으로 브로드캐스트 실패 * `RATE_LIMITED`: 속도 제한 초과 * `QUEUE_FULL`: 서버 큐가 용량에 도달 * `TIMEOUT`: 요청 시간 초과 ### 다음 권장 사항 * [**제로 가스 트랜잭션**](/ko/how-to/zero-gas-transactions) — 가스 수수료가 0임을 보여주는 영수증과 함께하는 데모 중심 안내. * [**가스 무료 트랜잭션 활성화**](/ko/how-to/integrate-gas-waiver) — 배치 제출 및 오류 처리를 포함한 전체 호스팅 API 통합 가이드. * [**자체 호스팅 가스 면제**](/ko/how-to/self-hosted-gas-waiver) — 호스팅 API 없이 자체 waiver 인프라를 실행합니다. ## 인덱서 인덱서와 분석 플랫폼은 온체인 데이터에 대한 구조화된 접근을 제공하여, 개발자가 트랜잭션, 잔액, 로그, 이벤트, 애플리케이션별 데이터를 대규모로 쿼리할 수 있게 합니다. Stable은 EVM 호환이므로 표준 Ethereum 인덱싱 도구가 원활하게 작동합니다. 이 페이지에서는 현재 및 향후 인덱싱 제공업체와 함께 개발자가 기대할 수 있는 기능을 나열합니다. ### 개요 표 | **제공업체** | **카테고리** | **문서 / 시작하기** | **참고** | | :------------------------------------------------------------------- | :------------ | :--------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- | | [**Stablescan**](https://stablescan.xyz/) | 블록체인 익스플로러 | [https://docs.etherscan.io/introduction](https://docs.etherscan.io/introduction) | 블록체인 익스플로러; 트랜잭션, 블록, 컨트랙트 가시성. | | [**The Graph**](https://thegraph.com/explorer/participants/indexers) | 인덱서 | [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) | GraphQL을 사용해 서브그래프를 구축, 배포, 쿼리합니다. | | [**Goldsky**](https://goldsky.com/) | 인덱서 | [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) | 고성능 인덱싱 및 실시간 데이터 스트리밍. | | [**Ormi Labs**](https://ormilabs.com/) | 인덱서 | [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) | 실시간 데이터 기능을 갖춘 차세대 서브그래프 인덱서. | | [**Allium**](https://www.allium.so/) | 분석 / 데이터 플랫폼 | [https://docs.allium.so/](https://docs.allium.so/) | 정규화된 블록체인 데이터셋 및 분석 도구. | | [**CoinMarketCap**](https://coinmarketcap.com/api/) | 시장 데이터 애그리게이터 | [https://coinmarketcap.com/api/documentation/v1/](https://coinmarketcap.com/api/documentation/v1/) | 시장 가격, 목록, 추적 도구. | | [**CoinGecko**](https://www.coingecko.com/en/api) | 시장 데이터 애그리게이터 | [https://docs.coingecko.com/](https://docs.coingecko.com/) | 개발자 API를 갖춘 독립적인 시장 데이터. | | [**Dexscreener**](https://docs.dexscreener.com/) | DEX 분석 | [https://docs.dexscreener.com/](https://docs.dexscreener.com/) | 실시간 DEX 차트, 유동성 분석, 대시보드. | | [**DeBank**](https://debank.com/) | 포트폴리오 / 지갑 분석 | [https://cloud.debank.com/](https://cloud.debank.com/) | EVM 지갑 추적, 트랜잭션, 포트폴리오 인사이트. | ### 1. 인덱서 인덱서는 원시 블록체인 데이터를 검색 가능하고 쿼리 가능한 형식으로 변환합니다. 대시보드, 분석, 지갑, 블록 익스플로러, 애플리케이션 백엔드를 구동합니다. #### The Graph 75,000개 이상의 프로젝트에 데이터 접근을 제공하는 탈중앙화 인덱싱 프로토콜. **기능** * Stable용 서브그래프 * GraphQL 기반 쿼리 * 분산 인덱싱 네트워크 **문서** 이 빠른 시작 가이드를 따라 몇 분 안에 서브그래프를 생성, 배포, 쿼리하세요: [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) #### Ormi Labs Ormi는 실시간 및 과거 블록체인 데이터를 대규모로 제공하도록 구축된 차세대 인덱서입니다. **기능** * 체인 최신 시점 인덱싱 * 1초 미만의 쿼리 지연 시간 * 노코드 기능 **문서** Ormi로 실시간 데이터 쿼리를 시작하세요: [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) Stable에서 USDT0 데이터를 실시간으로 쿼리하는 방법을 알아보세요: [https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0](https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0) #### Goldsky 즉시 사용 가능한 서브그래프와 개발자 도구를 갖춘 고성능 인덱싱 플랫폼. **기능** * 서브그래프 배포 및 관리 * 실시간 이벤트 스트리밍을 위한 웹훅 생성 * 외부 데이터베이스로의 다중 서브그래프 동기화 **문서** [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) 연중무휴 지원은 [support@goldsky.com](mailto\:support@goldsky.com)으로 문의하세요. ### 2. 분석 제공업체 분석 도구는 팀이 네트워크 활동, 실제 결제, 대시보드, 사용 흐름, 컨트랙트 상호작용을 추적하는 데 도움을 줍니다. #### Allium 블록체인 생태계 전반의 엔지니어와 분석가를 위한 기초 데이터 플랫폼. **기능** * 정규화된 블록체인 데이터셋 * 분석 팀을 위한 쿼리 도구 * 엔터프라이즈 데이터 인프라 **문서** Allium의 데이터 플랫폼 및 API 리소스로 시작하세요: [https://www.allium.so/](https://www.allium.so/) **시작하기**: [allium.so](https://www.allium.so/)에서 가입하여 API 접근 권한을 얻은 다음, REST API를 사용해 정규화된 Stable 온체인 데이터셋을 쿼리하세요. #### CoinMarketCap 암호화폐 자산을 위한 세계 최대의 글로벌 시장 추적 플랫폼. **기능** * Stable 자산 가격 추적 * 포트폴리오 도구 * 시장 데이터 API **문서** CMC API 및 통합 리소스를 살펴보세요: [https://coinmarketcap.com/api/](https://coinmarketcap.com/api/) **시작하기**: [coinmarketcap.com/api](https://coinmarketcap.com/api/)에서 API 키를 등록하고 REST API를 통해 Stable 자산 가격과 시장 데이터를 쿼리하세요. #### CoinGecko 세계 최대의 독립적인 암호화폐 데이터 애그리게이터. **기능** * 시장 목록 및 가격 데이터 * 과거 분석 * 개발자를 위한 API 접근 **문서** CoinGecko의 API 문서에 접근하세요: [https://www.coingecko.com/en/api](https://www.coingecko.com/en/api) **시작하기**: [coingecko.com/en/api](https://www.coingecko.com/en/api)에서 무료 또는 Pro API 키를 받고 REST API를 통해 Stable 토큰 가격과 시장 데이터를 쿼리하세요. #### Dexscreener 탈중앙화 거래소를 위한 실시간 차트 및 분석. **기능** * 실시간 DEX 차트 * 유동성 및 페어 분석 * 트레이딩 대시보드 **문서** Dexscreener의 API 엔드포인트 및 개발자 도구를 살펴보세요: [https://docs.dexscreener.com/](https://docs.dexscreener.com/) **시작하기**: [Dexscreener API 문서](https://docs.dexscreener.com/)를 둘러보고 Stable 기반 DEX 풀의 실시간 페어 데이터, 유동성, 트레이딩 활동을 쿼리하세요. #### DeBank Ethereum 및 EVM 생태계를 위한 포트폴리오 추적기. **기능** * 지갑 분석 * 트랜잭션 요약 * 체인 전반의 포트폴리오 추적 **문서** DeBank의 API 레퍼런스 및 통합 문서를 읽어보세요: [https://docs.debank.com/](https://docs.debank.com/) **시작하기**: [DeBank Cloud](https://cloud.debank.com/)에 가입하여 API 키에 접근하고 Stable 지갑 잔액, 트랜잭션 기록, 포트폴리오 데이터를 쿼리하세요. *** Stable을 통합하는 인덱싱 또는 분석 플랫폼이 있으신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의하세요. ## 인보이스 정산 각 인보이스는 인보이스 메타데이터(인보이스 번호, 당사자, 금액, 만기일)에서 파생된 고유하고 결정론적인 nonce에 매핑됩니다. 이 nonce는 [ERC-3009](/ko/explanation/erc-3009)를 통해 정산을 구동하며, 기존 회계 시스템과 대사할 수 있는 변경 불가능한 영수증을 생성합니다. ### 작동 방식 구매자와 공급업체는 동일한 인보이스 메타데이터로부터 동일한 nonce를 각자 독립적으로 계산합니다. 결제를 조율하기 위한 외부 레지스트리가 필요하지 않습니다. nonce는 결정론적으로 파생됩니다: ``` nonce = keccak256(invoiceNumber, vendor, buyer, amount, dueDate) ``` 구매자가 이 nonce를 사용하여 ERC-3009 권한을 서명하면, 온체인 정산 이벤트가 위변조 방지 결제 영수증 역할을 합니다. #### 정산 흐름 1. **인보이스 발행**: 공급업체가 고유 번호, 금액, 만기일이 포함된 인보이스를 생성합니다. 2. **nonce 계산**: 양 당사자가 인보이스 메타데이터로부터 동일한 nonce를 각자 독립적으로 파생합니다. 3. **구매자 서명**: 구매자가 결정론적 nonce를 사용하여 ERC-3009 권한을 오프체인에서 서명합니다. `validBefore` 필드는 만기일에 유예 기간을 더한 값으로 설정할 수 있습니다. 4. **정산**: 구매자 또는 공급업체가 온체인에서 `transferWithAuthorization`을 제출합니다. 정산은 1초 미만으로 확정됩니다. 5. **대사**: 발생한 `AuthorizationUsed` 이벤트에는 nonce가 포함되어 있어 온체인 정산을 정확한 인보이스와 연결합니다. 동일한 트랜잭션의 `Transfer` 이벤트는 발신자, 수신자, 금액을 검증합니다. #### 이중 결제 방지 nonce는 결제 시 온체인에서 소비됩니다. 동일한 인보이스를 두 번 정산할 수 없으며, 이미 사용된 nonce로 권한을 재제출하면 되돌려집니다(revert). ### 차별점 전통적인 B2B 인보이스 발행은 은행 송금(영업일 기준 1\~5일), 수동 대사, 그리고 인보이스 자체에 연결된 암호학적 결제 증명이 없는 과정을 수반합니다. 결정론적 nonce를 사용하면 온체인 결제가 자체 문서화됩니다. nonce는 정산을 정확한 인보이스와 연결하고, 블록체인 이벤트 로그는 변경 불가능한 감사 추적을 제공합니다. | **측면** | **전통적 방식(은행 송금)** | **Stable (ERC-3009)** | | :----- | :------------------------ | :------------------------------------------ | | 정산 | 영업일 기준 1\~5일 | 1초 미만 | | 대사 | 은행 명세서와 수동 대조 | `AuthorizationUsed` 이벤트가 결제를 인보이스 nonce에 연결 | | 결제 증명 | 은행 확인서 | 인보이스에 암호학적으로 연결된 온체인 트랜잭션 | | 중개자 | 코레스폰던트 은행 | 없음 | | 수수료 | 송금 수수료($15\~45) + 환전 스프레드 | 약 0.00021 USDT0 (또는 Gas Waiver 적용 시 0) | **참고 항목:** * [ERC-3009 (Transfer With Authorization)](/ko/explanation/erc-3009) * [Gas Waiver](/ko/how-to/integrate-gas-waiver) ## JSON-RPC API ### eth\_namespace | API | support | | ------------------------------------------- | ------- | | eth\_syncing | ✅ | | eth\_gasPrice | ✅ | | eth\_maxPriorityFeePerGas | ✅ | | eth\_feeHistory | ✅ | | eth\_blobBaseFee | ❌ | | eth\_chainId | ✅ | | eth\_blockNumber | ✅ | | eth\_getBalance | ✅ | | eth\_getProof | ✅ | | eth\_getHeaderByNumber | ❌ | | eth\_getHeaderByHash | ❌ | | eth\_getBlockByNumber | ✅ | | eth\_getBlockByHash | ✅ | | eth\_getUncleByBlockNumberAndIndex | ❌ | | eth\_getUncleByBlockHashAndIndex | ❌ | | eth\_getUncleCountByBlockNumber | ❌ | | eth\_getUncleCountByBlockHash | ❌ | | eth\_getCode | ✅ | | eth\_getStorageAt | ✅ | | eth\_getBlockReceipts | ❌ | | eth\_call | ✅ | | eth\_simulateV1 | ❌ | | eth\_estimateGas | ✅ | | eth\_createAccessList | ❌ | | eth\_getBlockTransactionCountByNumber | ✅ | | eth\_getBlockTransactionCountByHash | ✅ | | eth\_getTransactionByBlockNumberAndIndex | ✅ | | eth\_getTransactionByBlockHashAndIndex | ✅ | | eth\_getRawTransactionByBlockNumberAndIndex | ❌ | | eth\_getRawTransactionByBlockHashAndIndex | ❌ | | eth\_getTransactionCount | ✅ | | eth\_getTransactionByHash | ✅ | | eth\_getRawTransactionByHash | ❌ | | eth\_getTransactionReceipt | ✅ | | eth\_sendTransaction | ✅ | | eth\_fillTransaction | ❌ | | eth\_sendRawTransaction | ✅ | | eth\_sign | ✅ | | eth\_signTransaction | ❌ | | eth\_pendingTransactions | ✅ | | eth\_resend | ✅ | | eth\_accounts | ✅ | | eth\_subscribe | ✅ | | eth\_unsubscribe | ✅ | | eth\_getTransactionLogs | ✅ | | eth\_signTypedData | ✅ | | eth\_newPendingTransactionFilter | ✅ | | eth\_newBlockFilter | ✅ | | eth\_newFilter | ✅ | | eth\_getFilterChanges | ✅ | | eth\_getFilterLogs | ✅ | | eth\_uninstallFilter | ✅ | | eth\_getLogs | ✅ | ### debug\_namespace | API | support | | ---------------------------------- | ------- | | debug\_accountRange | ❌ | | debug\_backtraceAt | ❌ | | debug\_blockProfile | ✅ | | debug\_chaindbCompact | ❌ | | debug\_chaindbProperty | ❌ | | debug\_cpuProfile | ✅ | | debug\_dbAncient | ❌ | | debug\_dbAncients | ❌ | | debug\_dbGet | ❌ | | debug\_dumpBlock | ❌ | | debug\_freeOSMemory | ✅ | | debug\_freezeClient | ❌ | | debug\_gcStats | ✅ | | debug\_getAccessibleState | ❌ | | debug\_getBadBlocks | ❌ | | debug\_getRawBlock | ❌ | | debug\_getRawHeader | ❌ | | debug\_getRawTransaction | ❌ | | debug\_getModifiedAccountsByHash | ❌ | | debug\_getModifiedAccountsByNumber | ❌ | | debug\_getRawReceipts | ❌ | | debug\_goTrace | ✅ | | debug\_intermediateRoots | ✅ | | debug\_memStats | ✅ | | debug\_mutexProfile | ✅ | | debug\_preimage | ❌ | | debug\_printBlock | ✅ | | debug\_setBlockProfileRate | ✅ | | debug\_setGCPercent | ✅ | | debug\_setHead | ❌ | | debug\_setMutexProfileFraction | ✅ | | debug\_setTrieFlushInterval | ❌ | | debug\_stacks | ✅ | | debug\_standardTraceBlockToFile | ❌ | | debug\_standardTraceBadBlockToFile | ❌ | | debug\_startCPUProfile | ✅ | | debug\_startGoTrace | ✅ | | debug\_stopCPUProfile | ✅ | | debug\_stopGoTrace | ✅ | | debug\_storageRangeAt | ❌ | | debug\_traceBadBlock | ❌ | | debug\_traceBlock | ❌ | | debug\_traceBlockByNumber | ✅ | | debug\_traceBlockByHash | ✅ | | debug\_traceBlockFromFile | ❌ | | debug\_traceCall | ❌ | | debug\_traceChain | ❌ | | debug\_traceTransaction | ✅ | | debug\_verbosity | ❌ | | debug\_vmodule | ❌ | | debug\_writeBlockProfile | ✅ | | debug\_writeMemProfile | ✅ | | debug\_writeMutexProfile | ✅ | ## 메인넷 정보 Stable 메인넷에 접속하기 위해 알아야 할 모든 정보입니다. ### 네트워크 개요 | 구성 | 값 | | ------------ | -------------- | | **네트워크 이름** | Stable Mainnet | | **Chain ID** | `988` | | **가스 토큰** | USDT0 | | **거버넌스 토큰** | STABLE | | **블록 타임** | 약 0.7초 | ### 블록 익스플로러 | 익스플로러 | URL | | -------------- | ------------------------------------------------ | | **Stablescan** | [https://stablescan.xyz](https://stablescan.xyz) | ### RPC 엔드포인트 #### 기본 엔드포인트 | 유형 | 엔드포인트 | 용도 | | ---------------- | ------------------------------------------------ | -------- | | **EVM JSON-RPC** | [https://rpc.stable.xyz](https://rpc.stable.xyz) | EVM 트랜잭션 | | **WebSocket** | wss\://rpc.stable.xyz | 실시간 업데이트 | :::note 공개 RPC 엔드포인트는 **IP당 10초에 1,000건의 요청**으로 속도 제한이 적용됩니다. 제한을 초과하는 요청은 `HTTP 429`를 반환합니다. 더 높은 처리량이 필요하다면 [서드파티 RPC 제공자](/ko/reference/rpc-providers)를 사용하세요. ::: ### 체인 정보 | 파라미터 | EVM | | ------------ | ------- | | **Chain ID** | `988` | | **가스 토큰** | `USDT0` | | **소수점 자리수** | 18 | ### 도구 | 도구 | URL | 설명 | | ------- | ---------------------------------------------- | ------ | | **스냅샷** | [노드 운영자 가이드](/ko/how-to/use-node-snapshots) 참조 | 체인 스냅샷 | ## 버전 기록 Stable 메인넷의 전체 버전 기록 및 관련 문서입니다. ### 현재 버전 정보 * **현재 버전**: `v1.3.1` * **다음 업그레이드**: `TBD` * **업그레이드 높이**: `TBD` * **예상 시간**: `TBD` ### 버전 기록 #### 현재 및 이전 버전 | 버전 | 커밋 | 업그레이드 높이 | 바이너리 | 상태 | | ---------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | **v1.3.1** | `f85d155` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.1-linux-arm64-mainnet.tar.gz) | 현재 | | **v1.3.0** | `dd103ec` | 24,077,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.0-linux-arm64-mainnet.tar.gz) | | | **v1.2.2** | `76da1da` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-arm64-mainnet.tar.gz) | | | **v1.2.1** | `7955bb7` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-arm64-mainnet.tar.gz) | | | **v1.2.0** | `47e355b` | 12,004,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-arm64-mainnet.tar.gz) | | | **v1.1.4** | `c795773` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-arm64-mainnet.tar.gz) | | | **v1.1.2** | `3d83aa3` | 3,263,600 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-arm64-mainnet.tar.gz) | | | **v1.1.0** | `17ceaa7` | 1,694,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-arm64-mainnet.tar.gz) | | | **v1.0.0** | `d996084` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-arm64-mainnet.tar.gz) | Genesis | ### 관련 문서 * [업그레이드 가이드](/ko/how-to/upgrade-node) - 단계별 업그레이드 절차 * [메인넷 정보](/ko/reference/mainnet-information) - 현재 네트워크 세부 정보 ## 네트워크 라우팅 Stable의 애플리케이션을 위한 연결성과 데이터 전송을 최적화하는 네트워크 라우팅 제공업체입니다. ### 개요 표 | **제공업체** | **카테고리** | **문서 / 시작하기** | **참고** | | :---------------------------------- | :-------- | :---------------------------------- | :--------------- | | [**Optimum**](https://optimum.xyz/) | 탈중앙화 네트워킹 | [optimum.xyz](https://optimum.xyz/) | dApp을 위한 고성능 라우팅 | ### Optimum 속도와 확장 가능한 web3 상호작용에 최적화된 탈중앙화 인터넷 프로토콜입니다. **기능** * 고성능 탈중앙화 네트워킹 * 더 빠른 애플리케이션 데이터 라우팅 * dApp을 위한 안정적인 인프라 **시작하기**: [optimum.xyz](https://optimum.xyz/)를 방문하여 Optimum의 탈중앙화 네트워크 인프라를 통해 Stable dApp 트래픽을 라우팅하는 방법을 알아보세요. *** Stable과의 네트워킹 통합이 있으신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 연락 주세요. ## 네트워크 업그레이드 이 가이드는 다양한 사용 사례에 맞는 최적화를 포함하여 Stable 노드의 모든 구성 옵션을 다룹니다. ### 구성 파일 개요 Stable 노드는 두 개의 주요 구성 파일을 사용합니다: * **`config.toml`**: 핵심 StableBFT 구성 * **`app.toml`**: 애플리케이션 전용 구성 두 파일 모두 `~/.stabled/config/`에 위치합니다. ### 핵심 구성 (config.toml) #### 기본 설정 :::code-group ```toml [Mainnet] # The ID of the chain to join chain_id = "stable_988-1" # A custom human-readable name for this node moniker = "your-node-name" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb db_backend = "goleveldb" ``` ```toml [Testnet] # The ID of the chain to join chain_id = "stabletestnet_2201-1" # A custom human-readable name for this node moniker = "your-node-name" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb db_backend = "goleveldb" ``` ::: #### P2P 구성 :::code-group ```toml [Mainnet] [p2p] # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial external_address = "YOUR_PUBLIC_IP:26656" # Comma separated list of seed nodes seeds = "17a539fda42863a99755547e1c9b3ec4c38a4439@seed1.stable.xyz:26656" # Comma separated list of persistent peers persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656" ``` ```toml [Testnet] [p2p] # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial external_address = "YOUR_PUBLIC_IP:26656" # Comma separated list of seed nodes seeds = "39e061b167162f6621ddadcf1be21d6fa585a468@seed1.testnet.stable.xyz:26656" # Comma separated list of persistent peers persistent_peers = "5ed0f977a26ccf290e184e364fb04e268ef16430@peer1.testnet.stable.xyz:26656" ``` ::: 추가 P2P 설정 (두 네트워크에서 동일): ```toml # Maximum number of inbound peers max_num_inbound_peers = 50 # Maximum number of outbound peers max_num_outbound_peers = 30 # Toggle to disable guard against peers connecting from the same ip allow_duplicate_ip = false # Peer connection configuration handshake_timeout = "20s" dial_timeout = "3s" # Time to wait before flushing messages out on the connection flush_throttle_timeout = "100ms" # Maximum size of a message packet payload max_packet_msg_payload_size = 1024 # Rate limiting send_rate = 5120000 # 5 MB/s recv_rate = 5120000 # 5 MB/s # Seed mode (for seed nodes only) seed_mode = false # Enable peer exchange reactor pex = true ``` #### RPC 서버 구성 ```toml [rpc] # TCP or UNIX socket address for the RPC server laddr = "tcp://127.0.0.1:26657" # A list of origins a cross-domain request can be executed from cors_allowed_origins = ["*"] # A list of methods the client is allowed to use with cross-domain requests cors_allowed_methods = ["HEAD", "GET", "POST"] # A list of non simple headers the client is allowed to use with cross-domain requests cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] # TCP or UNIX socket address for the gRPC server grpc_laddr = "tcp://127.0.0.1:9090" # Maximum number of simultaneous connections grpc_max_open_connections = 900 # Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool unsafe = false # Maximum number of simultaneous connections (including WebSocket) max_open_connections = 900 # Maximum number of unique clientIDs that can connect max_subscription_clients = 100 # Maximum number of unique queries a given client can subscribe to max_subscriptions_per_client = 5 # How long to wait for a tx to be committed timeout_broadcast_tx_commit = "10s" # Maximum size of request body max_body_bytes = 1000000 # Maximum size of request header max_header_bytes = 1048576 ``` #### Mempool 구성 ```toml [mempool] # Mempool version to use version = "v1" # Recheck enabled recheck = true # Broadcast enabled broadcast = true # Maximum number of transactions in the mempool size = 3000 # Limit the total size of all txs in the mempool max_txs_bytes = 1073741824 # 1GB # Size of the cache cache_size = 10000 # Do not remove invalid transactions from the cache keep-invalid-txs-in-cache = false # Maximum size of a single transaction max_tx_bytes = 1048576 # 1MB # Maximum size of a batch of transactions to send to a peer max_batch_bytes = 0 ``` #### 합의 구성 ```toml [consensus] # How long we wait for a proposal block before prevoting nil timeout_propose = "5s" # How much timeout_propose increases with each round timeout_propose_delta = "10ms" # How long we wait after receiving +2/3 prevotes timeout_prevote = "150ms" # How much the timeout_prevote increases with each round timeout_prevote_delta = "10ms" # How long we wait after receiving +2/3 precommits timeout_precommit = "150s" # How much the timeout_precommit increases with each round timeout_precommit_delta = "10ms" # Make progress as soon as we have all the precommits skip_timeout_commit = false # Enable/disable double sign check double_sign_check_height = 2 # EmptyBlocks mode create_empty_blocks = true create_empty_blocks_interval = "0s" # Reactor sleep duration peer_gossip_sleep_duration = "100ms" peer_query_maj23_sleep_duration = "2s" ``` ### 애플리케이션 구성 (app.toml) #### 기본 애플리케이션 설정 ```toml # Pruning strategy pruning = "default" # HaltHeight contains a non-zero block height at which a node will halt halt-height = 0 # HaltTime contains a non-zero time at which a node will halt halt-time = 0 # MinRetainBlocks defines the number of blocks for which a node will retain min-retain-blocks = 0 # InterBlockCache enables inter-block caching inter-block-cache = true # IndexEvents defines the set of events in the form {eventType}.{attributeKey} index-events = [] # IavlCacheSize set the size of the iavl tree cache iavl-cache-size = 781250 ``` #### API 구성 ```toml [api] # Enable defines if the API server should be enabled enable = true # Swagger defines if swagger documentation should automatically be registered swagger = true # Address defines the API server to listen on address = "tcp://0.0.0.0:1317" # MaxOpenConnections defines the number of maximum open connections max-open-connections = 1000 # EnabledUnsafeCORS defines if CORS should be enabled enabled-unsafe-cors = true ``` #### gRPC 구성 ```toml [grpc] # Enable defines if the gRPC server should be enabled enable = true # Address defines the gRPC server address to bind to address = "0.0.0.0:9090" ``` #### EVM JSON-RPC 구성 ```toml [json-rpc] # Enable the JSON-RPC server enable = true # Address to bind the JSON-RPC server address = "0.0.0.0:8545" # Address to bind the WebSocket server ws-address = "0.0.0.0:8546" # APIs to enable api = "eth,net,web3,txpool,personal,debug" # Gas cap for eth_call/estimateGas gas-cap = 25000000 # EVM timeout for eth_call/estimateGas evm-timeout = "5s" # Tx fee cap for transactions txfee-cap = 1 # Filter cap for eth_getLogs filter-cap = 200 # FeeHistory cap feehistory-cap = 100 # Block range cap for eth_getLogs logs-cap = 10000 # Block range cap block-range-cap = 10000 # HTTP timeout http-timeout = "30s" # HTTP idle timeout http-idle-timeout = "120s" # Allow unprotected transactions allow-unprotected-txs = true # Maximum number of transactions in the pool max-tx-in-pool = 3000 # Enable indexer enable-indexer = false # Enable metrics metrics = true ``` ### 구성 프로파일 #### 풀 노드 (기본값) 풀 노드를 위한 균형 잡힌 구성: ```bash # config.toml adjustments sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 50/' ~/.stabled/config/config.toml sed -i 's/^max_num_outbound_peers = .*/max_num_outbound_peers = 30/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^pruning = ".*"/pruning = "default"/' ~/.stabled/config/app.toml sed -i 's/^snapshot-interval = .*/snapshot-interval = 1000/' ~/.stabled/config/app.toml ``` #### 아카이브 노드 프루닝 없음, 전체 이력: ```bash # config.toml adjustments sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^pruning = ".*"/pruning = "nothing"/' ~/.stabled/config/app.toml ``` #### RPC 노드 공개 RPC 엔드포인트 구성: ```bash # config.toml adjustments sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 30/' ~/.stabled/config/config.toml sed -i 's/^max_open_connections = .*/max_open_connections = 30/' ~/.stabled/config/config.toml sed -i 's/^cors_allowed_origins = .*/cors_allowed_origins = ["*"]/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^enable = .*/enable = true/' ~/.stabled/config/app.toml sed -i 's/^swagger = .*/swagger = true/' ~/.stabled/config/app.toml sed -i 's/^enabled-unsafe-cors = .*/enabled-unsafe-cors = true/' ~/.stabled/config/app.toml ``` ### 모니터링 구성 #### Prometheus 메트릭 ```toml # config.toml [instrumentation] # Enable Prometheus metrics prometheus = true # Metrics listen address prometheus_listen_addr = ":26660" # Namespace for metrics namespace = "stablebft" ``` #### 로깅 ```toml # config.toml [log] # Log level (trace|debug|info|warn|error|fatal|panic) level = "info" # Log format (plain|json) format = "plain" ``` ### 구성 변경 사항 적용 구성을 변경한 후: ```bash # Restart the node sudo systemctl restart ${SERVICE_NAME} # Check logs for errors sudo journalctl -u ${SERVICE_NAME} -f # Verify configuration loaded curl localhost:26657/status | jq '.result.node_info' ``` ### 다음 단계 * 노드를 위한 [모니터링 설정](/ko/how-to/monitor-node) * 일반적인 문제에 대해 [문제 해결 가이드](/ko/how-to/troubleshoot-node) 검토 ## 운영 운영은 Stable 노드 실행을 다룹니다: 풀 노드 또는 아카이브 노드, 테스트넷 또는 메인넷, 설치부터 모니터링까지. 노드가 적용하는 체인 수준 동작(수수료 모델, 파이널리티, 가스로서의 USDT0)에 대해서는 [가스 가격 책정](/ko/explanation/gas-pricing), [파이널리티](/ko/explanation/finality), [아키텍처 개요](/ko/explanation/core-optimization-overview)를 참조하세요. ### 빠른 링크 * **[시스템 요구사항](/ko/reference/node-system-requirements)** - 다양한 노드 유형에 대한 하드웨어 및 소프트웨어 요구사항 * **[설치 가이드](/ko/how-to/install-node)** - 다양한 플랫폼에 대한 단계별 설치 지침 * **[구성](/ko/reference/node-configuration)** - 상세한 구성 옵션 및 모범 사례 * **[스냅샷 및 동기화](/ko/how-to/use-node-snapshots)** - 스냅샷을 사용한 빠른 동기화 옵션 * **[밸리데이터 생성](/ko/how-to/run-validator)** - 동기화된 노드를 밸리데이터로 등록하고 자체 위임 * **[업그레이드 가이드](/ko/how-to/upgrade-node)** - 노드 업그레이드 절차 및 버전 기록 * **[모니터링](/ko/how-to/monitor-node)** - 노드 모니터링을 위한 도구 및 메트릭 * **[문제 해결](/ko/how-to/troubleshoot-node)** - 일반적인 문제 및 해결책 `stabled`를 실행하는 대신 체인에서 밸리데이터 데이터(스테이크, 가동 시간, 투표 기록)를 읽으려면 [밸리데이터 데이터 인덱싱](/ko/how-to/index-validator-data)을 참조하세요. ### 노드 유형 #### 풀 노드 풀 노드는 블록체인의 완전한 복사본을 유지하며 모든 트랜잭션과 블록을 검증합니다. 풀 노드는: * 모든 트랜잭션과 블록을 검증 * 전체 블록체인 기록을 유지 * 다른 노드에 데이터를 제공할 수 있음 * 네트워크의 탈중앙화를 지원 #### 아카이브 노드 아카이브 노드는 모든 상태의 완전한 기록을 저장하며 과거 쿼리를 제공할 수 있습니다. 아카이브 노드는: * 모든 과거 상태를 저장 * 모든 블록 높이에서 과거 쿼리를 지원 * 상당히 더 많은 스토리지를 요구 * 블록 탐색기 및 분석에 필수적 ### 네트워크 정보 RPC 엔드포인트, 블록 탐색기, 체인 파라미터를 포함한 전체 네트워크 세부 정보는 다음을 참조하세요: * **[메인넷](/ko/reference/mainnet-information)** - 메인넷 세부 정보 * **[테스트넷](/ko/reference/testnet-information)** - 테스트넷 세부 정보 ### 지원 및 커뮤니티 * **Discord**: [Stable Discord 참여하기](https://discord.gg/stablexyz) ### 빠른 시작 빠르게 시작하고자 하는 숙련된 운영자를 위해: 1. [시스템 요구사항](/ko/reference/node-system-requirements) 확인 2. [설치 가이드](/ko/how-to/install-node) 따르기 3. [구성 가이드](/ko/reference/node-configuration)를 사용하여 노드 구성 4. [스냅샷](/ko/how-to/use-node-snapshots)으로 동기화 속도 향상 5. [모니터링 가이드](/ko/how-to/monitor-node)로 노드 모니터링 네트워크 파라미터 및 RPC 엔드포인트는 [메인넷 정보](/ko/reference/mainnet-information) 또는 [테스트넷 정보](/ko/reference/testnet-information)를 참조하세요. ### 노드 운영이 체인과 연결되는 방식 노드를 실행한다는 것은 Stable의 체인 수준 규칙을 적용한다는 의미입니다. 다음 페이지에서는 노드가 구현하는 동작을 설명합니다: * **[컨트랙트 개요](/ko/explanation/contracts-overview)** 는 노드가 제공하는 수수료 모델, JSON-RPC 표면, 시스템 모듈을 다룹니다. * **[파이널리티](/ko/explanation/finality)** 는 단일 슬롯 파이널리티와 합의 계층에서 "확정"이 무엇을 의미하는지 설명합니다. * **[아키텍처 개요](/ko/explanation/core-optimization-overview)** 는 합의, 실행, 데이터베이스, RPC 계층을 살펴봅니다. * **[가스 가격 책정](/ko/explanation/gas-pricing)** 은 USDT0 단위 수수료가 어떻게 책정되고 징수되는지 설명합니다. 이 페이지는 다양한 유형의 Stable 노드를 실행하기 위한 하드웨어 및 소프트웨어 요구사항을 설명합니다. ### 하드웨어 요구사항 #### 풀노드 (최소 사양) | 구성 요소 | 요구사항 | 참고사항 | | -------- | ----------------------------- | ------------------------------ | | **CPU** | 4코어 | AMD Ryzen 5 / Intel Core i5 이상 | | **RAM** | 8 GB | 16 GB 권장 | | **스토리지** | 500 GB NVMe/SSD | | | **네트워크** | 100 Mbps | | | **OS** | Ubuntu 22.04/24.04, Debian 12 | 64bit Linux 필요 | #### 풀노드 (권장 사양) | 구성 요소 | 요구사항 | 참고사항 | | -------- | ------------ | ------------------------------ | | **CPU** | 8코어 | AMD Ryzen 7 / Intel Core i7 이상 | | **RAM** | 16 GB | 32 GB 권장 | | **스토리지** | 1 TB NVMe | | | **네트워크** | 1 Gbps | | | **OS** | Ubuntu 24.04 | 최신 LTS 권장 | #### 아카이브 노드 | 구성 요소 | 요구사항 | 참고사항 | | -------- | ------------ | --------------------------------- | | **CPU** | 16코어 | AMD Ryzen 9 / Intel Core i9 또는 동급 | | **RAM** | 32 GB | 64 GB 권장 | | **스토리지** | 4 TB NVMe | 추후 확장 가능한 구조여야함 | | **네트워크** | 1 Gbps | | | **OS** | Ubuntu 24.04 | 최신 LTS 권장 | ### 소프트웨어 요구사항 #### 운영 체제 ##### 지원되는 배포판 * **Ubuntu 24.04 LTS** (권장) * **Ubuntu 22.04 LTS** * **Debian 12 (Bookworm)** ##### 시스템 의존성 ```bash # 시스템 패키지 업데이트 sudo apt update && sudo apt upgrade -y # 필수 도구 설치 sudo apt install -y \ build-essential \ git \ wget \ curl \ jq \ lz4 \ zstd \ htop \ net-tools \ ufw ``` ### 클라우드 프로바이더 별 권장 타입 #### AWS * **풀노드**: t3.xlarge 또는 c5.xlarge * **아카이브 노드**: m5.2xlarge 또는 c5.2xlarge * **스토리지**: 프로비저닝된 IOPS를 가진 gp3 #### Google Cloud * **풀노드**: n2-standard-4 * **아카이브 노드**: n2-standard-8 * **스토리지**: pd-ssd 또는 pd-extreme #### Azure * **풀노드**: Standard\_D4s\_v5 * **아카이브 노드**: Standard\_D8s\_v5 * **스토리지**: Premium SSD v2 #### DigitalOcean * **풀노드**: General Purpose 8GB * **아카이브 노드**: CPU-Optimized 16GB * **스토리지**: Volume Block Storage ### 모니터링 요구사항 프로덕션 배포를 위해 다음을 확보하세요: * **Prometheus**: 메트릭 수집용 * **Grafana**: 시각화용 * **AlertManager**: 알림용 * **Node Exporter**: 시스템 메트릭용 * **로그 집계**: Loki 권장 ### 보안 고려사항 #### 시스템 강화 * OS 및 패키지를 최신 상태로 유지 * 자동 보안 업데이트 구성 * SSH 키만 사용 (비밀번호 인증 비활성화) * fail2ban 구성 * 방화벽 활성화 (UFW/iptables) ### 설치 전 체크리스트 설치를 진행하기 전에 다음을 확인하세요: * [ ] 하드웨어가 최소 요구사항을 충족 * [ ] 운영 체제가 지원되고 업데이트됨 * [ ] 스토리지가 충분한 IOPS를 보유 * [ ] 네트워크 대역폭이 1Gbps 이상임 * [ ] 방화벽이 구성되어있음 * [ ] 모니터링이 세팅됨 * [ ] 백업 계획 준비됨 * [ ] 보안 조치가 마련됨 ## 오라클 오라클은 자산 가격과 같은 오프체인 데이터를 스마트 컨트랙트에 제공합니다. RedStone은 Stable에서 가격 피드를 운영합니다. ### 개요 표 | **제공자** | **카테고리** | **지원 페어** | **문서 / 시작하기** | **비고** | | :-------------------------------------------- | :-------- | :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | :------- | | [**RedStone**](https://www.redstone.finance/) | 오라클 가격 피드 | BTC, ETH, USDT, USDC, PYUSD, XAUt, frxUSD, FXS, LBTC, sfrxETH/ETH, sfrxUSD, SolvBTC, sthUSD, thBILL, weETH | [https://docs.redstone.finance/docs/dapps/redstone-push/](https://docs.redstone.finance/docs/dapps/redstone-push/) | 메인넷 가동 중 | ### RedStone RedStone은 [Push 모델](https://docs.redstone.finance/docs/dapps/redstone-push/)을 통해 Stable에서 오라클 가격 피드를 제공합니다. 피드 컨트랙트는 Chainlink 호환 `AggregatorV3Interface`를 노출합니다. **기능** * 구성 가능한 편차 임계값 및 하트비트 간격을 갖춘 Push 기반 가격 피드 * `latestRoundData()`, `decimals()`, `description()`을 갖춘 Chainlink 호환 `AggregatorV3Interface` * 블루칩 자산, 스테이블코인, LST, LRT, 이자 발생 / 펀더멘털 가격 자산에 대한 커버리지 #### 메인넷 가격 피드 주소 출처: [RedStone push 피드 대시보드](https://app.redstone.finance/push-feeds?networks=stable\&testnets=true) | **가격 피드** | **컨트랙트 주소** | **편차** | **하트비트** | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------- | :----- | :------- | | **BTC / USD** | [0x687103bA8CC2f66C94696182Ef410400Da45fb24](https://stablescan.xyz/address/0x687103bA8CC2f66C94696182Ef410400Da45fb24) | 0.5% | 6h | | **ETH / USD** | [0x457BE3C697c644bF329C2C3ea79EbF1D254d603a](https://stablescan.xyz/address/0x457BE3C697c644bF329C2C3ea79EbF1D254d603a) | 0.5% | 6h | | **USDT / USD** | [0x58264801fadCd8598D3EE993572ADe9cA27F42c8](https://stablescan.xyz/address/0x58264801fadCd8598D3EE993572ADe9cA27F42c8) | 0.5% | 6h | | **USDC / USD** | [0x8ea3C667C264BbdaA1dA7638904b8671F451c7F9](https://stablescan.xyz/address/0x8ea3C667C264BbdaA1dA7638904b8671F451c7F9) | 0.5% | 6h | | **PYUSD / USD** | [0x1c30dA143E97c228102A5cAe3960dBBB41321604](https://stablescan.xyz/address/0x1c30dA143E97c228102A5cAe3960dBBB41321604) | 0.5% | 6h | | **XAUt / USD** | [0xd5E244accc514b56DCAD89897DD44499E7C35a05](https://stablescan.xyz/address/0xd5E244accc514b56DCAD89897DD44499E7C35a05) | 0.5% | 6h | | **frxUSD / USD** | [0xB5197ca89507FE045e8ce9996593D35071915EB7](https://stablescan.xyz/address/0xB5197ca89507FE045e8ce9996593D35071915EB7) | 0.5% | 6h | | **FXS / USD** | [0xC3b182aee94AECeCa39b072942f3Ce4B87465517](https://stablescan.xyz/address/0xC3b182aee94AECeCa39b072942f3Ce4B87465517) | 0.5% | 6h | | **LBTC / USD** | [0x80295Cf12E28f3F943304BFd6C2A2C044e731aaB](https://stablescan.xyz/address/0x80295Cf12E28f3F943304BFd6C2A2C044e731aaB) | 0.5% | 6h | | **sfrxETH / ETH** | [0x29533E113D803ab1967F6CB9495B95DC8C1EA594](https://stablescan.xyz/address/0x29533E113D803ab1967F6CB9495B95DC8C1EA594) | 0.5% | 6h | | **sfrxUSD / FUNDAMENTAL** | [0x71784611831b9566df7301A78bC1B3d29a8737bF](https://stablescan.xyz/address/0x71784611831b9566df7301A78bC1B3d29a8737bF) | 0.5% | 6h | | **SolvBTC / FUNDAMENTAL** | [0x58fa68A373956285dDfb340EDf755246f8DfCA16](https://stablescan.xyz/address/0x58fa68A373956285dDfb340EDf755246f8DfCA16) | 0.01% | 24h | | **sthUSD / FUNDAMENTAL** | [0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db](https://stablescan.xyz/address/0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db) | 0.5% | 12h | | **thBILL / FUNDAMENTAL / USD** | [0x7532df197a36587aeD2B9A59785c8BeD182FA62D](https://stablescan.xyz/address/0x7532df197a36587aeD2B9A59785c8BeD182FA62D) | 0.5% | 6h | | **weETH / FUNDAMENTAL** | [0xD57b79401956BE4872D3d03F0C920639335e350F](https://stablescan.xyz/address/0xD57b79401956BE4872D3d03F0C920639335e350F) | 0.5% | 6h | ### 가격 피드 읽기 피드 컨트랙트는 Chainlink 호환 [`AggregatorV3Interface`](https://docs.redstone.finance/docs/dapps/redstone-push/)를 구현합니다. 모든 Chainlink 스타일 가격 피드에 사용되는 동일한 컨슈머 패턴이 적용됩니다. ```solidity pragma solidity ^0.8.25; interface AggregatorV3Interface { function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ); function decimals() external view returns (uint8); function description() external view returns (string memory); } contract OracleConsumer { AggregatorV3Interface public oracle; constructor(address oracleAddress) { oracle = AggregatorV3Interface(oracleAddress); } function getLatestPriceData() external view returns ( uint80 roundId, int256 answer, uint256 updatedAt ) { (roundId, answer, , updatedAt, ) = oracle.latestRoundData(); return (roundId, answer, updatedAt); } } ``` :::note Stable의 RedStone push 피드는 현재 메인넷에서만 사용할 수 있습니다. 가격을 사용하기 전에 항상 온체인 `updatedAt`을 애플리케이션의 신선도 허용 범위와 비교하여 검증하세요. ::: #### 피드 직접 읽기 컨슈머 컨트랙트를 배포하지 않고도 모든 RedStone 피드를 읽을 수 있습니다. 다음 호출은 ETH/USD 피드를 읽습니다. ```bash cast call 0x457BE3C697c644bF329C2C3ea79EbF1D254d603a "latestRoundData()(uint80,int256,uint256,uint256,uint80)" --rpc-url https://rpc.stable.xyz ``` #### Stable 메인넷에 컨슈머 배포하기 이 과정은 Foundry가 설치되어 있고 자금이 들어 있는 지갑이 있다고 가정합니다. 전체 설정 안내는 [스마트 컨트랙트 배포](/ko/tutorial/smart-contract) 튜토리얼을 참조하세요. 1. 위 컨트랙트를 Foundry 프로젝트의 `src/OracleConsumer.sol`에 저장합니다. 2. BTC/USD 메인넷 피드 주소로 배포합니다. ```bash source .env ; forge create src/OracleConsumer.sol:OracleConsumer --broadcast --rpc-url $STABLE_MAINNET_RPC_URL --private-key $PRIVATE_KEY --constructor-args 0x687103bA8CC2f66C94696182Ef410400Da45fb24 ``` 3. 배포된 컨트랙트에서 최신 가격을 읽습니다. ```bash cast call "getLatestPriceData()(uint80,int256,uint256)" --rpc-url $STABLE_MAINNET_RPC_URL ``` ### Stable을 통합하는 오라클이 있으신가요? 이 페이지에 등재되려면 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 팀에 연락하세요. ## USDT0 보내기 및 받기 Stable에서는 P2P 결제가 1초 이내에 정산됩니다. 사용 사례에 따라 두 가지 전송 방식을 사용할 수 있습니다. ### 네이티브 전송 발신자가 트랜잭션에 서명하고 수신자에게 직접 브로드캐스트합니다. 이 작업은 21,000 가스(100 gwei에서 \~0.00021 USDT0)가 소요됩니다. 컨트랙트 상호작용이 필요하지 않습니다. 네이티브 전송은 가장 간단한 경로입니다. 발신자가 USDT0를 수신자에게 직접 보내는 트랜잭션에 서명합니다. 이는 결제 앱에서 금액을 입력하고 "보내기"를 선택하는 것과 동일합니다. 코드 설명은 [첫 USDT0 보내기](/ko/tutorial/send-usdt0)를 참조하세요. ### 애플리케이션 시작 전송 (ERC-3009) 발신자가 오프체인 권한 부여에 서명합니다. 애플리케이션 또는 퍼실리테이터가 발신자를 대신하여 트랜잭션을 제출합니다. [가스 면제](/ko/how-to/integrate-gas-waiver)와 결합하면 가스 비용은 0입니다. [ERC-3009](/ko/explanation/erc-3009)는 서명자와 제출자를 분리하기 때문에 애플리케이션이 시작하는 결제(예: 웹 앱에서의 결제)에 더 적합합니다. 발신자는 오프체인에서 권한 부여에만 서명하고, 애플리케이션 또는 퍼실리테이터가 온체인 제출을 처리합니다. ### 차별점 전통적인 결제 레일에서는 P2P 전송에 은행 처리, 청산, 정산이 포함되어 영업일 기준 1\~3일이 걸릴 수 있습니다. 다른 블록체인에서도 발신자는 결제 토큰과 함께 변동성이 큰 가스 토큰(ETH, SOL)을 보유해야 합니다. Stable에서는 발신자가 USDT0만 보유하고, 가스를 면제받을 수 있으며, 정산은 1초 이내에 최종 확정됩니다. | **측면** | **전통적 방식 (은행 송금)** | **다른 블록체인** | **Stable** | | :------ | :----------------- | :----------------------------- | :-------------------------------------- | | 정산 시간 | 영업일 기준 1\~3일 | 체인에 따라 수 초에서 수 분 | 1초 미만 (단일 슬롯 최종성) | | 필요 자산 | 법정 화폐 | 결제 토큰 + 별도의 가스 토큰 (ETH, SOL 등) | USDT0만 (단일 자산) | | 트랜잭션 비용 | 전신 송금 / 중개자 수수료 | 네트워크 혼잡 시 급증할 수 있음 | 네이티브 전송 \~0.00021 USDT0 또는 가스 면제를 통한 $0 | **참고 자료:** * [첫 USDT0 보내기](/ko/tutorial/send-usdt0) * [ERC-3009 (권한 부여 전송)](/ko/explanation/erc-3009) * [가스 면제](/ko/how-to/integrate-gas-waiver) ## API 요청별 과금 [x402](/ko/explanation/x402) 미들웨어를 사용하여 요청별 가격 책정으로 모든 HTTP 엔드포인트를 수익화하세요. 서버는 가격을 선언하고, 클라이언트는 호출마다 결제하며, 정산은 요청 수명 주기 내에서 이루어집니다. 계정도, API 키도, 청구 주기도 필요 없습니다. ### 작동 방식 서버는 수익화하려는 엔드포인트에 x402 미들웨어를 추가합니다. 결제 없이 요청이 도착하면 서버는 HTTP `402 Payment Required`와 함께 가격, 토큰, 네트워크를 담은 `PAYMENT-REQUIRED` 헤더로 응답합니다. 클라이언트는 지정된 금액에 대해 [ERC-3009](/ko/explanation/erc-3009) 인가에 서명하고 다시 요청을 제출합니다. 퍼실리테이터가 온체인에서 결제를 정산하면 서버가 리소스를 반환합니다. #### 요청 흐름 1. 클라이언트가 서버에 HTTP 요청을 보냅니다. 2. 서버가 가격, 토큰, 네트워크, 수신자를 담은 `PAYMENT-REQUIRED` 헤더와 함께 `402 Payment Required`를 반환합니다. 3. 클라이언트가 지정된 금액에 대해 ERC-3009 인가에 서명하고 `PAYMENT-SIGNATURE` 헤더와 함께 요청을 다시 제출합니다. 4. 퍼실리테이터가 서명을 검증하고 온체인에서 전송을 정산합니다. 5. 서버가 정산 영수증을 담은 `PAYMENT-RESPONSE` 헤더와 함께 리소스를 반환합니다. #### 가격 책정 가격은 USDT0 원자 단위(소수점 6자리)로 표시됩니다. 비용 매개변수 `"1000"`은 정확히 $0.001로 변환됩니다. 비용 `"50000"`은 $0.05입니다. 이 정밀도 덕분에 서버는 1센트의 일부 수준으로 가격을 설정할 수 있습니다. #### 인프라 Stable에서는 [Semantic Pay](https://x402.semanticpay.io)가 공개 퍼실리테이터를 운영합니다. 개발자는 자체 정산 인프라를 운영하지 않고도 이 엔드포인트로 미들웨어를 연결할 수 있습니다. x402는 Express(`@x402/express`), Hono(`@x402/hono`), Next.js(`@x402/next`)용 미들웨어를 제공합니다. 모든 프레임워크에서 패턴은 동일합니다: 퍼실리테이터 클라이언트를 생성하고, EVM 스킴을 등록하며, 미들웨어를 적용합니다. ### 무엇이 다른가 전통적인 API 수익화는 사용자 등록, API 키 관리, 사용량 추적, 청구 주기, 결제 처리업체 통합을 필요로 합니다. x402를 사용하면 서버가 각 엔드포인트에 결제 핸들러를 연결하고, 클라이언트는 요청마다 결제하며, 정산은 동일한 HTTP 수명 주기 내에서 완료됩니다. 서버는 클라이언트가 누구인지 알 필요가 없으며, 유효한 결제가 제출되었다는 사실만 알면 됩니다. | **측면** | **전통적 방식 (API 키 + 청구 주기)** | **Stable (x402)** | | :------------- | :-------------------------------- | :----------------------- | | 서버 측 설정 | 등록, API 키, 사용량 추적, 청구 주기, 결제 처리업체 | 엔드포인트별 x402 결제 핸들러 | | 클라이언트 온보딩 | 계정 생성, API 키 발급 | 없음 (지갑만 필요) | | 청구 모델 | 월별 또는 사용량 기반 청구 | 요청별 정산 | | 클라이언트 신원 필요 여부 | 예 (API 키) | 아니오 (유효한 결제만) | | 정산 | 청구 주기 종료 시 | 요청 수명 주기 내 (1초 미만) | | 최소 실행 가능 가격 | 약 $0.30 (카드 처리 하한선) | $0.001 (USDT0 원자 단위) | | 클라이언트 유형 | 사람 사용자만 (가입 필요) | 모든 지갑: 사람, AI 에이전트, 스크립트 | **참고 자료:** * [x402 (HTTP 네이티브 결제)](/ko/explanation/x402) * [ERC-3009 (인가를 통한 전송)](/ko/explanation/erc-3009) * [가스 면제](/ko/how-to/integrate-gas-waiver) ## 램프 온/오프 램프 파트너는 Stable을 글로벌 법정화폐 시스템에 연결하여 사용자와 기업이 USDT, 현지 통화, 결제 레일 간에 자금을 이동할 수 있게 합니다. ### 온/오프 램프 개요 표 | **제공업체** | **카테고리** | **문서 / 시작하기** | **참고** | | :----------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- | | [**Onmeta**](https://onmeta.in/on-off-ramp) | 온/오프 램프 | [https://docs.onmeta.in/](https://docs.onmeta.in/) | 규정 준수 법정화폐-USDT 전환 + 국가 간 지급 | | [**Halliday**](https://halliday.xyz/) | 온/오프 램프 | [https://docs.halliday.xyz/pages/home](https://docs.halliday.xyz/pages/home) | CEX + Stripe 통합 | | [**Alchemy Pay**](https://alchemypay.org/about) | 게이트웨이 | [https://alchemypay.readme.io/docs/alchemypay-on-ramp](https://alchemypay.readme.io/docs/alchemypay-on-ramp) | 300개 이상의 결제 방식 | | [**DFX**](https://www.dfx.swiss/) | 오프 램프 FX | [https://docs.dfx.swiss/](https://docs.dfx.swiss/) | 규제 대상 스테이블코인-법정화폐 | | [**Onramp Money**](https://onramp.money/) | 온/오프 램프 | [https://docs.onramp.money/onramp/](https://docs.onramp.money/onramp/) | 신흥 시장 커버리지 | | [**MoonPay**](https://www.moonpay.com/business/onramps) | 유니버설 램프 | [https://dev.moonpay.com/docs/on-ramp-overview](https://dev.moonpay.com/docs/on-ramp-overview) | 글로벌 카드 + 은행 지원 | | [**Transak**](https://transak.com/off-ramp) | 온/오프 램프 | [https://docs.transak.com/](https://docs.transak.com/) | 450개 이상의 통합 | | [**Banxa**](https://banxa.com/solutions/by-use-case/on-and-off-ramping/) | 규제 대상 램프 | [https://docs.banxa.com/docs/overview](https://docs.banxa.com/docs/overview) | 100개 이상 국가의 현지 레일 | | [**Simplex by Nuvei**](https://www.simplex.com/) | 온 램프 | [https://buy.simplex.com](https://buy.simplex.com) | 245개 이상의 시장에서 카드, Apple/Google Pay, SEPA, ACH 지원; Atomic Wallet을 포함한 하위 지갑 구동 | ### 카테고리 가이드 * **온/오프 램프:** Stable에서 법정화폐와 USDT 간의 직접 전환을 가능하게 하는 플랫폼. * **결제 게이트웨이:** 원활한 거래를 위해 앱, 가맹점 또는 핀테크를 글로벌 법정화폐 레일에 연결하는 서비스. * **FX 및 오프 램프 네트워크:** 대규모로 규정을 준수하는 스테이블코인-법정화폐 결제를 제공하는 규제 대상 인프라. ### 온/오프 램프 및 결제 게이트웨이 #### MoonPay 즉각적인 자산 구매를 위해 전 세계적으로 사용되는 유니버설 암호화폐 온/오프 램프. **기능** * 카드 + 은행 + 현지 결제 레일 * 빠른 글로벌 온보딩 * 멀티체인 지원 **시작하기**: [MoonPay 온 램프 통합 가이드](https://dev.moonpay.com/docs/on-ramp-overview)를 따라 Stable에서의 법정화폐-USDT 구매를 앱에 내장하세요. #### Transak 450개 이상의 앱에서 암호화폐를 매매하고 전송하는 마찰 없는 방법을 제공하는 온/오프 램프. **기능** * 글로벌 법정화폐 방식 * 손쉬운 통합 API * 국가별 규정 준수 지원 **시작하기**: [Transak 통합 문서](https://docs.transak.com/)를 검토하여 Stable을 지원 네트워크로 사용하는 온/오프 램프 위젯 또는 API를 추가하세요. #### Onmeta VDA 플랫폼을 위한 규정 준수 기능이 내장된 온/오프 램프 및 국가 간 지급 인프라. **기능** * 법정화폐 ↔ USDT 전환 * 규정 준수 가능한 흐름 * 국가 간 지급 * 현지 결제 레일 **시작하기**: [Onmeta API 문서](https://docs.onmeta.in/)를 참조하여 Stable에서 법정화폐-USDT 전환 및 국가 간 지급을 통합하세요. #### Halliday Coinbase 및 Stripe와 같은 주요 CEX 및 결제 플랫폼에서 원활한 온/오프 램프를 가능하게 하는 종단 간 결제 제품군. **기능** * 주요 CEX에 직접 연결 * Stripe + 결제 레일 통합 * 가맹점 및 앱 친화적 흐름 **시작하기**: [Halliday 통합 문서](https://docs.halliday.xyz/pages/home)를 읽고 CEX 및 Stripe 기반 결제 흐름을 제품 내 Stable에 연결하세요. #### Alchemy Pay 300개 이상의 결제 방식으로 전통 금융과 암호화폐를 연결하는 글로벌 결제 게이트웨이. **기능** * 글로벌 법정화폐 온/오프 램프 * 가맹점 결제 * 은행 송금 + 카드 + 현지 레일 **시작하기**: [Alchemy Pay 온 램프 API](https://alchemypay.readme.io/docs/alchemypay-on-ramp)를 사용하여 Stable에서 USDT 구매를 위한 300개 이상의 결제 방식을 추가하세요. #### DFX 1억 건 이상의 거래를 보유한 스위스 규제 대상 탈중앙화 FX 및 오프 램프 네트워크. **기능** * 스테이블코인-법정화폐 전환 * 규제 대상 FX 레이어 * 강력한 다중 통화 지원 **시작하기**: [DFX API 문서](https://docs.dfx.swiss/)를 참고하여 Stable을 소스 네트워크로 사용하는 규정 준수 스테이블코인-법정화폐 오프 램프 흐름을 설정하세요. #### Onramp Money 신흥 시장에 특화된 저비용 법정화폐-암호화폐 게이트웨이. **기능** * 현지 결제 방식 커버리지 * 즉시 결제 * 130만 명 이상의 지원 사용자 **시작하기**: [Onramp Money 개발자 가이드](https://docs.onramp.money/onramp/)를 따라 Stable에서 신흥 시장 사용자를 위한 법정화폐-암호화폐 흐름을 통합하세요. #### Banxa 100개 이상 국가의 현지 레일을 통해 사용자를 암호화폐에 연결하는 규제 대상 온/오프 램프 인프라. **기능** * 규제 대상 법정화폐 게이트웨이 * 광범위한 글로벌 커버리지 * 은행 및 현지 결제 옵션 **시작하기**: [Banxa 통합 개요](https://docs.banxa.com/docs/overview)를 읽고 Stable을 지원 네트워크로 사용하는 규제 대상 온/오프 램프 흐름을 활성화하세요. #### Simplex by Nuvei 245개 이상의 시장에서 카드, 은행 및 현지 결제 방식을 지원하는 Nuvei의 법정화폐-암호화폐 온 램프. Simplex를 통한 Stable에서의 USDT 구매는 [Atomic Wallet](/ko/reference/wallets#atomic-wallet)을 포함하여 Simplex 위젯을 통합한 모든 지갑이나 앱 내에서도 가능합니다. **기능** * Visa, Mastercard, Apple Pay, Google Pay, SEPA, ACH를 통한 Stable에서의 USDT 구매 * 245개 이상의 시장에 걸친 글로벌 커버리지 * 하위 지갑 및 앱에서 사용되는 화이트라벨 위젯(예: Atomic Wallet) **시작하기**: [https://buy.simplex.com](https://buy.simplex.com)에서 Stable의 USDT를 직접 구매하거나, [Simplex 위젯](https://www.simplex.com/)을 통합하여 자체 앱 내에서 온 램프 구매를 제공하세요. *** Stable을 통합하는 온/오프 램프가 있으신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의해 주세요. ## RPC 제공자 Stable을 지원하는 RPC 및 개발자 인프라 제공자입니다. ### 개요 표 | **제공자** | **카테고리** | **문서 / 시작하기** | **비고** | | :--------------------------------------------------------------------------------------------------------------- | :------------ | :----------------------------------------------------------------------------------------------------------------- | :------------------------ | | [**Alchemy**](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC + 개발자 플랫폼 | [Alchemy로 시작하기](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC, WebSocket, 모니터링, SDK | | [**Tenderly**](https://tenderly.co/) | 시뮬레이션 + 디버깅 | [tenderly.co](https://tenderly.co/) | 실시간 시뮬레이션, 추적, 트랜잭션 워크플로 | ### Alchemy 전 세계적으로 신뢰받는 완전한 블록체인 개발 플랫폼입니다. [Alchemy로 시작하기](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) **기능** * RPC + WebSocket 인프라 * 모니터링 대시보드 * 개발자 API 및 SDK **시작하기**: [Alchemy 대시보드](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable)에서 Stable 앱을 생성하여 RPC URL을 받은 다음, 이를 JSON-RPC 엔드포인트로 사용하세요. ### Tenderly 시뮬레이션, 디버깅, 모니터링, 실행 도구를 제공하는 풀스택 개발자 플랫폼입니다. **기능** * 실시간 컨트랙트 시뮬레이션 * 디버깅 및 추적 * 개발자를 위한 트랜잭션 워크플로 **시작하기**: [Tenderly 대시보드](https://tenderly.co/)에서 Stable 프로젝트를 설정하여 컨트랙트에 대한 시뮬레이션, 디버깅, 트랜잭션 추적 기능을 사용하세요. *** Stable과의 RPC 또는 인프라 통합이 있으신가요? [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 문의해 주세요. ## SDK 레퍼런스 `@stablechain/sdk`의 전체 표면. 단계별 안내는 [SDK 빠른 시작](/ko/tutorial/sdk-quickstart)을 참조하세요. ### 설치 ```bash npm install @stablechain/sdk viem ``` ```text added 2 packages, audited 3 packages in 2s ``` `viem >= 2.0.0`은 피어 의존성입니다. ### `createStable(config)` `StableClient`를 생성합니다. [`StableClient`](#stableclient)에 나열된 메서드를 가진 객체를 반환합니다. ```ts import { createStable, Network } from "@stablechain/sdk"; const stable = createStable({ network: Network.Mainnet, account }); ``` ```text StableClient { transfer, quoteBridge, bridge, quoteSwap, swap } ``` #### `StableConfig` | **필드** | **타입** | **기본값** | **설명** | | :------------- | :------------------ | :---------------- | :------------------------------------------------ | | `network` | `Network` | `Network.Mainnet` | 대상 네트워크. | | `rpc` | `string` | `network`의 공개 RPC | RPC 재정의. | | `account` | `viem.Account` | | 서버 측 서명자 (예: `privateKeyToAccount`). | | `transport` | `viem.Transport` | | 브라우저 지갑 트랜스포트 (예: `custom(window.ethereum)`). | | `walletClient` | `viem.WalletClient` | | 사전 구축된 지갑 클라이언트. `account` 및 `transport`보다 우선합니다. | `account`, `transport`, `walletClient` 중 하나를 제공하세요. ### `StableClient` #### `transfer(params)` Stable에서 네이티브 USDT0 또는 임의의 ERC-20을 전송합니다. 지갑을 Stable 체인으로 전환하고, 누락된 경우 온체인에서 토큰 소수 자릿수를 가져오며, 영수증을 기다립니다. ```ts const { txHash } = await stable.transfer({ from: "0xYourAddress", to: "0xRecipient", amount: 10, }); ``` ```text { txHash: "0x8f3a...2d41" } ``` | **파라미터** | **타입** | **설명** | | :-------------- | :-------- | :--------------------------------- | | `from` | `string` | 발신자 주소. | | `to` | `string` | 수신자 주소. | | `amount` | `number` | 사람이 읽을 수 있는 금액. | | `token` | `string?` | ERC-20 컨트랙트 주소. 네이티브 USDT0의 경우 생략. | | `tokenDecimals` | `number?` | 소수 자릿수. 생략 시 온체인에서 가져옵니다. | `OperationResult` (`{ txHash, toAmount? }`)를 반환합니다. #### `quoteBridge(params)` 브리지를 미리 봅니다. 읽기 전용 — 서명도, 가스도 없습니다. ```ts const quote = await stable.quoteBridge({ fromChain: Chain.Ethereum, toChain: Chain.Stable, fromToken: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, }); ``` ```text { toAmount: 99.94 } ``` `BridgeQuote`를 반환합니다. #### `bridge(params)` 토큰을 크로스체인으로 브리지합니다. SDK가 경로를 선택합니다: USDT0 → USDT0에는 LayerZero, 그 외 모든 경우에는 LI.FI를 사용합니다. 내부 견적 호출을 건너뛰려면 사전에 가져온 `quote`를 전달하세요. ```ts const { txHash } = await stable.bridge({ ...bridgeParams, quote }); ``` ```text { txHash: "0xabcd...7890" } ``` | **파라미터** | **타입** | **설명** | | :------------- | :------------- | :--------------------------- | | `fromChain` | `Chain` | 소스 체인. | | `toChain` | `Chain` | 대상 체인. | | `fromToken` | `string` | 소스 토큰 컨트랙트 주소. | | `toToken` | `string` | 대상 토큰 컨트랙트 주소. | | `amount` | `number` | 사람이 읽을 수 있는 금액. | | `fromDecimals` | `number?` | 소스 토큰 소수 자릿수. 기본값은 `6`. | | `recipient` | `string?` | 대상 주소. 기본값은 서명자. | | `quote` | `BridgeQuote?` | 사전에 가져온 견적. 내부 견적 호출을 건너뜁니다. | #### `quoteSwap(params)` Stable에서 LI.FI 스왑 견적을 가져옵니다. 사전 구축된 트랜잭션 요청과 승인 주소를 반환합니다. ```ts const quote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, }); ``` ```text { toAmount: 99.81, fromAmount: 100000000n, fromToken: "0x8a2B...", approvalAddress: "0x...", transactionRequest: { ... } } ``` #### `swap(params)` LI.FI를 통해 Stable에서 토큰을 스왑합니다. ERC-20 승인을 자동으로 처리하고 필요한 경우 지갑의 체인을 전환합니다. ```ts const { txHash, toAmount } = await stable.swap({ ...swapParams, quote }); ``` ```text { txHash: "0xabcd...", toAmount: 99.81 } ``` | **파라미터** | **타입** | **기본값** | **설명** | | :------------- | :----------- | :------ | :--------------------------- | | `fromToken` | `string` | | 소스 토큰 주소. | | `toToken` | `string` | | 대상 토큰 주소. | | `amount` | `number` | | 사람이 읽을 수 있는 금액. | | `fromDecimals` | `number?` | `6` | 소스 토큰 소수 자릿수. | | `toAddress` | `string?` | 서명자 | 수신자 주소. | | `quote` | `SwapQuote?` | | 사전에 가져온 견적. LI.FI 호출을 건너뜁니다. | ### 열거형 #### `Network` | **값** | **체인 ID** | | :---------------- | :-------- | | `Network.Mainnet` | 988 | | `Network.Testnet` | 2201 | #### `Chain` `quoteBridge`와 `bridge`에서 사용됩니다. 각 항목에는 해당하는 `CHAIN_CONFIGS` 항목이 있습니다. | **열거형** | **네트워크** | **체인 ID** | | :-------------------- | :--------------- | :-------- | | `Chain.Sepolia` | Ethereum Sepolia | 11155111 | | `Chain.StableTestnet` | Stable Testnet | 2201 | | `Chain.Stable` | Stable Mainnet | 988 | | `Chain.Ethereum` | Ethereum | 1 | | `Chain.Arbitrum` | Arbitrum One | 42161 | | `Chain.Ink` | Ink | 57073 | | `Chain.Bera` | Berachain | 80094 | | `Chain.MegaETH` | MegaETH | 4326 | | `Chain.Base` | Base | 8453 | | `Chain.BSC` | BNB Smart Chain | 56 | | `Chain.HyperEVM` | HyperEVM | 999 | ### `CHAIN_CONFIGS` `Chain` 열거형을 키로 하는 `Partial>`입니다. 각 항목은 `id`, `rpc`, `usdt`, `decimals`를 노출합니다. 지원되는 체인의 정식 USDT 주소를 하드코딩하지 않고 필요할 때 사용하세요. ```ts import { CHAIN_CONFIGS, Chain } from "@stablechain/sdk"; console.log(CHAIN_CONFIGS[Chain.Stable]); ``` ```text { id: 988, rpc: "https://rpc.stable.xyz", usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", decimals: 6 } ``` ### 오류 모든 SDK 오류는 viem의 `BaseError`를 확장하는 `StableError`를 확장합니다. 오류는 구조화된 메타데이터를 포함하므로 `error.name` 또는 `instanceof`로 분기할 수 있습니다. | **클래스** | **발생 시점** | **유용한 필드** | | :----------------------- | :------------------------------------------------ | :----------------------------------------------- | | `StableValidationError` | 파라미터가 검증에 실패할 때 (잘못된 주소, 유한하지 않은 금액, 지원되지 않는 체인). | `field`, `value` | | `StableQuoteError` | LI.FI에 대한 견적 요청이 실패할 때. | `provider`, `httpStatus`, `providerCode`, `body` | | `StableTransactionError` | 온체인 단계가 실패할 때: 체인 전환, 승인, 전송 또는 되돌리기. | `phase`, `txHash`, `chainId`, `revertReason` | | `StableNetworkError` | 기본 HTTP/RPC 호출이 실패할 때. | `url` | ```ts import { StableTransactionError } from "@stablechain/sdk"; try { await stable.transfer({ from, to, amount: 1 }); } catch (err) { if (err instanceof StableTransactionError && err.phase === "switch_chain") { // user rejected the chain switch } throw err; } ``` ```text StableTransactionError: transfer: wallet rejected or failed to switch to chain 988 Phase: switch_chain Chain ID: 988 ``` ### 다음 추천 항목 * [**SDK 빠른 시작**](/ko/tutorial/sdk-quickstart) — SDK를 설치하고 테스트넷에서 첫 전송을 실행합니다. * [**viem과 함께 사용하기**](/ko/how-to/sdk-with-viem) — 개인 키, 브라우저 지갑, 사전 구축된 서명자 간 전환. * [**wagmi와 함께 사용하기**](/ko/how-to/sdk-with-wagmi) — 훅을 사용하여 React 앱에 SDK를 연결합니다. ## 스테이킹 프리컴파일 레퍼런스 :::note **개념:** 스테이킹 모듈이 무엇을 하는지와 언제 사용하는지는 [스테이킹 모듈](/ko/explanation/staking-module)을 참고하세요. ::: ### 개요 `staking` 프리컴파일 컨트랙트는 EVM 환경에서 Stable SDK의 `x/staking` 모듈 기능을 사용할 수 있게 해주는 브리지 역할을 합니다. ### 목차 1. **[개념](#concepts)** 2. **[구성](#configuration)** 3. **[메서드](#methods)** 4. **[이벤트](#events)** ### 개념 Stable SDK의 `x/staking` 모듈에서는 스테이킹을 위해 체인 초기화 중에 본드 denom을 등록해야 합니다. 검증자와 위임자는 본드 denom 스테이킹 토큰만 사용할 수 있습니다. `staking` 프리컴파일 컨트랙트는 검증자 또는 위임자가 호출자인지 확인하기 위한 추가 검사를 수행합니다. ### 구성 컨트랙트 주소와 가스 비용은 미리 정의되어 있습니다. #### 컨트랙트 주소 * `0x0000000000000000000000000000000000000800` ### 메서드 #### `createValidator` 검증자를 생성합니다. 검증자는 운영자의 초기 위임으로 생성되어야 합니다. 잠재적 위임자를 위해 검증자는 수수료율에 대한 정보와 계획을 제공해야 합니다. 위임자는 공개된 정보를 바탕으로 토큰을 위임할 검증자를 선택할 수 있으며, 시장 메커니즘에 의한 자연스러운 규제가 이루어집니다. `CreateValidator`는 검증자가 성공적으로 등록될 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ----------------- | --------------- | --------------------------- | | description | Description | 검증자의 정보 | | commissionRates | CommissionRates | 검증자가 보상받는 스테이킹 토큰의 수수료율 | | minSelfDelegation | uint256 | 검증자의 최소 자기 위임 금액 | | validatorAddress | address | 검증자의 주소 | | pubkey | string | 검증자의 공개 키 | | value | uint256 | 검증자에게 초기에 자기 위임된 스테이킹 토큰의 양 | `Description`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------------- | ------ | ------------- | | moniker | string | 검증자의 이름 | | identity | string | 검증자의 신원 | | website | string | 검증자 웹사이트의 url | | securityContact | string | 보안 연락처 정보 | | details | string | 검증자에 대한 추가 설명 | `CommissionRates`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------------- | ------- | ------------------------- | | rate | uint256 | 검증자가 받는 현재 수수료율 | | maxRate | uint256 | 최대 수수료율(이보다 높게 설정할 수 없음) | | maxChangeRate | uint256 | 검증자가 하루에 변경할 수 있는 최대 수수료율 | `rate`는 시장이 수용할 수 있는 적절한 값으로 설정해야 합니다. * 검증자의 수수료율이 높을수록 위임자의 이익은 낮아집니다. * 검증자의 수수료율이 낮을수록 검증자의 이익이 낮아지고 운영이 어려워집니다. 높은 `maxRate`는 위임자가 검증자의 예상치 못한 높은 수수료율을 우려하게 만들 수 있으므로 `maxRate`는 신중하게 설정해야 합니다. `maxChangeRate`는 초기화된 후에는 변경할 수 없습니다. ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | -------------------- | | success | bool | 검증자가 성공적으로 등록되면 true | #### `editValidator` 검증자가 자신의 정보를 업데이트합니다. 검증자는 `CommissionRates` 구조체의 `maxRate`와 `maxChangeRate` 같은 변경 불가능한 필드를 제외한 정보만 업데이트할 수 있습니다. `EditValidator`는 검증자가 성공적으로 업데이트될 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ----------------- | ----------- | ----------------------- | | description | Description | 검증자의 정보 | | validatorAddress | address | 검증자의 주소 | | commissionRate | int256 | 검증자가 보상받는 스테이킹 토큰의 수수료율 | | minSelfDelegation | int256 | 검증자의 최소 자기 위임 금액 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ---------------------- | | success | bool | 검증자가 성공적으로 업데이트되면 true | #### `delegate` 위임자가 검증자에게 위임할 토큰의 양을 설정합니다. `Delegate`는 위임이 성공적으로 완료될 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | -------------------- | | delegatorAddress | address | 위임자의 주소 | | validatorAddress | address | 검증자의 주소 | | amount | uint256 | 검증자에게 위임된 스테이킹 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 위임이 성공적으로 완료되면 true | ##### 이벤트 `newShares`는 위임자의 소유 비율을 나타냅니다. 동일한 양의 토큰이 위임되더라도 시점에 따라 계산된 share가 달라질 수 있습니다. #### `undelegate` 위임자가 검증자에게 위임한 토큰의 양을 출금합니다. `Unbond`는 위임 해제가 성공적으로 완료될 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | -------------------------- | | delegatorAddress | address | 위임자의 주소 | | validatorAddress | address | 검증자의 주소 | | amount | uint256 | 검증자로부터 위임 해제하려는 스테이킹 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ---------------------- | | success | bool | 위임 해제가 성공적으로 완료되면 true | #### `redelegate` 위임자가 검증자에게 위임한 토큰의 양을 다른 검증자로 재위임합니다. `Redelegate`는 재위임이 성공적으로 완료될 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | --------------- | | delegatorAddress | address | 위임자의 주소 | | validatorSrc | string | 출발 검증자의 주소 | | validatorDst | string | 도착 검증자의 주소 | | amount | uint256 | 재위임할 스테이킹 토큰의 양 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | -------------------- | | success | bool | 재위임이 성공적으로 완료되면 true | #### `delegation` 위임자와 검증자 간의 위임 정보를 반환합니다. 위임이 발견되지 않으면 `shares`와 `balance`는 `0`이 됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | delegatorAddress | address | 위임자의 주소 | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ------- | ---------------- | | shares | uint256 | 위임된 share | | balance | Coin | 위임된 토큰의 양과 denom | `Coin`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------ | ------- | --------- | | denom | string | 보상의 denom | | amount | uint256 | 보상의 양 | #### `unbondingDelegation` 위임자와 검증자 간의 위임 해제 정보를 반환합니다. 위임 해제가 발견되지 않으면 빈 `UnbondingDelegationOutput`이 반환됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | delegatorAddress | address | 위임자의 주소 | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------------------- | ------------------------- | --------- | | unbondingDelegation | UnbondingDelegationOutput | 위임 해제의 정보 | `UnbondingDelegationOutput`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ---------------- | --------------------------- | ---------- | | validatorAddress | address | 검증자의 주소 | | delegatorAddress | address | 위임자의 주소 | | entries | UnbondingDelegationEntry\[] | 위임 해제의 엔트리 | `UnbondingDelegationEntry`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | -------------- | ------ | ---------- | | creationHeight | uint64 | 엔트리의 생성 높이 | | completionTime | uint64 | 엔트리의 완료 시간 | | initialBalance | Coin | 엔트리의 초기 잔액 | | balance | Coin | 엔트리의 잔액 | #### `validator` 검증자 정보를 반환합니다. 검증자가 발견되지 않으면 빈 `ValidatorOutput`이 반환됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | --------- | --------- | ------- | | validator | Validator | 검증자의 정보 | `Validator`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ----------------- | ------- | ----------------------- | | operatorAddress | address | 검증자의 주소 | | consensusPubkey | string | 검증자의 공개 키 | | jailed | bool | 검증자가 jail 상태인지 여부 | | status | int32 | 검증자의 상태 | | tokens | uint256 | 검증자에게 위임된 스테이킹 토큰의 양 | | delegatorShares | uint256 | 위임 share의 양 | | description | string | 검증자의 설명 | | unbondingHeight | int64 | 검증자가 위임 해제되는 높이 | | unbondingTime | int64 | 검증자가 위임 해제되는 시간 | | commission | uint256 | 검증자가 보상받는 스테이킹 토큰의 수수료율 | | minSelfDelegation | uint256 | 검증자의 최소 자기 위임 금액 | #### `validators` 상태와 일치하는 모든 검증자를 반환합니다. 검증자가 발견되지 않으면 빈 `ValidatorsOutput`이 반환됩니다. `x/staking` 모듈에 선언된 상태는 다음 중 하나일 수 있습니다: * 0 : "BOND\_STATUS\_UNSPECIFIED", 지정되지 않은 상태 * 1 : "BOND\_STATUS\_UNBONDING", 검증자가 위임 해제 중 * 2 : "BOND\_STATUS\_UNBONDED", 검증자가 위임 해제됨 * 3 : "BOND\_STATUS\_BONDED", 검증자가 본딩됨 ##### 입력 | 이름 | 타입 | 설명 | | ----------- | ------- | --------- | | status | string | 검증자의 상태 | | pageRequest | PageReq | 페이지네이션 요청 | `PageReq`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ---------- | ----- | -------------- | | key | bytes | 페이지의 키 | | offset | int64 | 페이지의 오프셋 | | limit | int64 | 페이지의 한도 | | countTotal | bool | 전체 결과 수를 셀지 여부 | | reverse | bool | 결과를 역순으로 할지 여부 | ##### 출력 | 이름 | 타입 | 설명 | | ------------ | ------------ | --------- | | validators | Validator\[] | 검증자의 배열 | | pageResponse | PageResp | 페이지네이션 응답 | `PageResp`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------- | ------ | --------- | | nextKey | bytes | 페이지의 다음 키 | | total | uint64 | 전체 결과 수 | #### `redelegation` 위임자, 출발 검증자, 도착 검증자의 재위임 정보를 반환합니다. 재위임이 발견되지 않으면 빈 `RedelegationOutput`이 반환됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ------------------- | ------- | ---------- | | delegatorAddress | address | 위임자의 주소 | | srcValidatorAddress | address | 출발 검증자의 주소 | | dstValidatorAddress | address | 도착 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------------ | ------------------ | ------- | | redelegation | RedelegationOutput | 재위임의 정보 | `RedelegationOutput`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------------------- | -------------------- | ---------- | | delegatorAddress | address | 위임자의 주소 | | validatorSrcAddress | address | 출발 검증자의 주소 | | validatorDstAddress | address | 도착 검증자의 주소 | | entries | RedelegationEntry\[] | 재위임의 엔트리 | `RedelegationEntry`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | -------------- | ------ | ---------- | | creationHeight | uint64 | 엔트리의 생성 높이 | | completionTime | uint64 | 엔트리의 완료 시간 | | initialBalance | Coin | 엔트리의 초기 잔액 | | balance | Coin | 엔트리의 잔액 | #### `redelegations` 위임자, 출발 검증자, 도착 검증자의 모든 재위임을 반환합니다. 재위임이 발견되지 않으면 빈 `RedelegationResponse`와 `PageResp`가 반환됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ------------------- | ------- | ---------- | | delegatorAddress | address | 위임자의 주소 | | srcValidatorAddress | address | 출발 검증자의 주소 | | dstValidatorAddress | address | 도착 검증자의 주소 | | pageRequest | PageReq | 페이지네이션 요청 | ##### 출력 | 이름 | 타입 | 설명 | | ------------ | ----------------------- | --------- | | response | RedelegationResponse\[] | 재위임의 정보 | | pageResponse | PageResp | 페이지네이션 응답 | ### 이벤트 #### CreateValidator | 이름 | 타입 | 인덱싱됨 | 설명 | | -------- | ------- | ---- | --------------------------- | | valiAddr | address | Y | 검증자의 주소 | | value | uint256 | N | 검증자에게 초기에 자기 위임된 스테이킹 토큰의 양 | #### EditValidator | 이름 | 타입 | 인덱싱됨 | 설명 | | ----------------- | ------- | ---- | ----------------------------- | | valiAddr | address | Y | 검증자의 주소 | | commissionRate | int256 | N | 검증자가 보상받는 스테이킹 토큰의 업데이트된 수수료율 | | minSelfDelegation | int256 | N | 검증자의 업데이트된 최소 자기 위임 금액 | #### Delegate | 이름 | 타입 | 인덱싱됨 | 설명 | | ------------- | ------- | ---- | -------------------- | | delegatorAddr | address | Y | 위임자의 주소 | | validatorAddr | string | Y | 검증자의 주소 | | amount | uint256 | N | 검증자에게 위임된 스테이킹 토큰의 양 | | newShares | uint256 | N | 위임 후 위임 share의 양 | #### Unbond | 이름 | 타입 | 인덱싱됨 | 설명 | | -------------- | ------- | ---- | ------------------------ | | delegatorAddr | address | Y | 위임자의 주소 | | validatorAddr | string | Y | 검증자의 주소 | | amount | uint256 | N | 검증자로부터 위임 해제된 스테이킹 토큰의 양 | | completionTime | uint256 | N | 위임 해제의 완료 시간 | #### Redelegate | 이름 | 타입 | 인덱싱됨 | 설명 | | ------------------- | ------- | ---- | --------------- | | delegatorAddr | address | Y | 위임자의 주소 | | validatorSrcAddress | address | Y | 출발 검증자의 주소 | | validatorDstAddress | address | Y | 도착 검증자의 주소 | | amount | uint256 | N | 재위임할 스테이킹 토큰의 양 | | completionTime | uint256 | N | 재위임의 완료 시간 | ## 정기 결제 설정하기 풀(pull) 기반 구독을 사용하면 구독자가 매번 결제를 직접 시작하지 않아도 서비스 제공자가 정해진 일정에 따라 결제를 수금할 수 있습니다. 이 패턴은 [EIP-7702](/ko/reference/eip-7702-api) 계정 추상화로 가능합니다. 구독자의 EOA는 실행 권한을 구독 위임(delegate) 컨트랙트에 위임하며, 제공자는 각 청구 주기마다 이 컨트랙트를 호출합니다. 구독자는 단 두 번만 행동합니다: 구독할 때 한 번, 취소할 때 한 번. ### 작동 방식 구독자는 청구 조건을 집행하는 컨트랙트에 자신의 EOA를 위임합니다. EIP-7702를 통해 구독자의 계정은 일시적으로 컨트랙트 로직을 갖게 되어, 서비스 제공자가 구독자가 매번 서명하지 않아도 각 청구 주기마다 결제를 수금할 수 있습니다. #### 구독 생애주기 1. **위임**: 구독자가 EIP-7702를 통해 자신의 EOA를 구독 위임 컨트랙트에 위임합니다. 2. **구독**: 구독자가 청구 조건(제공자 주소, 주기당 금액, 청구 주기)을 등록합니다. 3. **수금**: 서비스 제공자가 각 청구 주기마다 수금을 트리거합니다. 위임 컨트랙트는 USDT0 전송을 실행하기 전에 호출자, 주기, 금액을 검증합니다. 4. **취소**: 구독자가 구독을 철회하거나 위임을 해제하여 향후 수금을 중단합니다. #### 중요 고려사항 * **지속적인 위임**: EIP-7702 위임은 구독자가 명시적으로 변경하거나 해제할 때까지 유지됩니다. 매 청구 주기마다 재위임이 필요하지 않습니다. * **EOA당 단일 위임**: EIP-7702는 EOA당 하나의 활성 위임만 지원합니다. 구독자가 나중에 다른 컨트랙트에 위임하면 구독 위임 로직이 대체되어 수금이 실패합니다. 단일 위임으로 여러 기능(구독, 일괄 결제, 지출 한도)을 지원하는 모듈식 위임 컨트랙트를 사용하세요. * **감사된 위임 컨트랙트 사용**: 위임 컨트랙트는 구독자의 EOA에 대한 완전한 실행 권한을 갖습니다. 감사를 받은 컨트랙트에만 위임하세요. ### 차별점 전통적인 구독은 카드 데이터를 저장하고, 실패한 청구를 재시도하며, 복잡한 청구 상태를 관리합니다. EIP-7702 구독에서는 청구 조건이 구독자 자신의 EOA에 있는 위임 로직에 의해 집행됩니다. 제공자는 주기당 합의된 금액만 수금할 수 있으며, 구독자는 위임을 철회하여 언제든지 취소할 수 있습니다. | **항목** | **전통 방식(카드 등록)** | **Stable** | | :--------- | :----------------- | :------------------- | | 설정 | 결제 처리업체에 카드 등록 | 단일 EIP-7702 위임 트랜잭션 | | 청구 | 처리업체가 저장된 카드에 청구 | 제공자가 위임 컨트랙트 호출 | | 저장된 결제 데이터 | 처리업체가 카드번호, CVV 보유 | 오프체인에 결제 자격 증명 저장 없음 | | 취소 | 제공자 또는 카드 발급사에 연락 | 구독자가 온체인에서 위임 철회 | | 과다 청구 위험 | 제공자 측 청구 통제에 의존 | 청구 조건이 컨트랙트로 집행됨 | **함께 보기:** * [EIP-7702](/ko/reference/eip-7702-api) * [ERC-3009 (승인을 통한 전송)](/ko/explanation/erc-3009) ## 시스템 모듈 참조 Stable은 가스 효율성과 예측 가능한 제어를 위해 **프리컴파일된 컨트랙트(precompiled contracts)** 로 구현된 **시스템 모듈(System Modules)** 을 통해 핵심 결제 동작을 노출합니다. :::note **개념:** 시스템 모듈이 무엇을 하고 왜 프리컴파일로 노출되는지에 대해서는 [시스템 모듈](/ko/explanation/system-modules-overview)을 참조하세요. ::: **주요 모듈:** * [Bank Module](/ko/reference/bank-module-api) * USDT 전송, 잔액 회계, 에스크로 흐름을 처리합니다 * [Distribution Module](/ko/reference/distribution-module-api) * 네트워크 참여자를 위한 수수료 분배 및 보상 로직 * [Staking Module](/ko/reference/staking-module-api) * 검증자 참여 및 스테이킹을 제어합니다 (메인넷과 함께 제공 예정) **dApp은 토큰 또는 결제 로직을 다시 구현하는 대신 내장 모듈을 활용할 수 있습니다.** ## 시스템 트랜잭션 레퍼런스 :::note **개념:** 시스템 트랜잭션이 SDK 이벤트를 EVM에 어떻게 연결하며 왜 중요한지에 대해서는 [시스템 트랜잭션](/ko/explanation/system-transactions)을 참조하세요. ::: ### 개요 시스템 트랜잭션은 Stable 프로토콜이 Stable SDK 작업에 대해 EVM 이벤트를 발생시킬 수 있는 방법을 제공합니다. 언본딩 완료와 같은 스테이킹 이벤트가 SDK 레이어에서 발생하면, 프로토콜은 자동으로 해당 이벤트를 발생시키는 EVM 트랜잭션을 생성합니다. 이를 통해 이러한 작업들이 EVM 도구 및 애플리케이션에 완전히 가시화됩니다. ### 동기 여러분과 Stable 위의 애플리케이션은 `eth_getLogs`와 같은 표준 EVM 인터페이스를 통해 블록체인 이벤트를 모니터링하기를 기대합니다. 하지만 중요한 작업들은 자연스럽게 EVM 이벤트를 발생시키지 않는 Stable SDK 모듈에서 일어납니다. 이로 인해 가시성 격차가 발생합니다: EVM dApp은 사용자의 토큰이 언제 언본딩을 마치는지 쉽게 추적할 수 없습니다. 시스템 트랜잭션은 이 격차를 메웁니다. 스테이킹 모듈이 언본딩 작업을 완료하면, Stable의 x/stable 모듈이 이벤트를 감지하고 StableSystem 프리컴파일(`0x0000000000000000000000000000000000009999`)을 호출하는 시스템 트랜잭션을 생성합니다. 그러면 프리컴파일은 어떤 dApp이든 구독할 수 있는 적절한 EVM 이벤트를 발생시킵니다. 시스템 트랜잭션은 프로토콜만 사용할 수 있는 특별한 발신자 주소(`0x8888888888888888888888888888888888888888`)로 실행됩니다. 이는 누구도 프로토콜 이벤트를 위조하지 못하도록 막으면서 이벤트 발생을 신뢰가 필요 없고 온체인에서 검증 가능하게 유지합니다. ### 사양 시스템 트랜잭션은 세 가지 주요 구성 요소를 통해 작동합니다: x/stable 모듈의 EndBlocker, PrepareProposal 핸들러, 그리고 StableSystem 프리컴파일입니다. #### 아키텍처 개요 시스템 트랜잭션 아키텍처 #### StableSystem 프리컴파일 StableSystem 프리컴파일은 `0x0000000000000000000000000000000000009999`에 위치하며 EVM 이벤트를 발생시켜야 하는 프로토콜 수준의 작업을 처리합니다. 현재는 언본딩 완료 알림을 지원합니다. ```solidity interface IStableSystem { /// @notice Processes queued unbonding completions and emits EVM events /// @param blockHeight The block height at which to process completions /// @dev Only callable by system transactions (from = 0x8888888888888888888888888888888888888888) /// @dev Processes up to 100 completions per call /// @dev Automatically deletes processed completions from the queue function notifyUnbondingCompletions(int64 blockHeight) external; /// @notice Emitted when an unbonding operation completes /// @param delegator The address that delegated the tokens /// @param validator The validator address the tokens were delegated to /// @param amount The amount of tokens that finished unbonding (in uusdc) event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); /// @notice The caller is not authorized (not system transaction sender) error Unauthorized(); } ``` #### 시스템 트랜잭션 발신자 시스템 트랜잭션은 `0x8888888888888888888888888888888888888888`을 발신자 주소로 사용합니다. 이 주소는: * 서명 검증이 필요하지 않습니다 * PrepareProposal에서 생성된 트랜잭션에서만 사용할 수 있습니다 * 사용자나 컨트랙트가 위조할 수 없습니다 * SystemTxDecorator ante 핸들러를 통해 수수료 공제를 건너뜁니다 EVM은 `msg.sender == 0x8888888888888888888888888888888888888888`을 확인하여 시스템 트랜잭션을 인식합니다. 프리컴파일은 이를 사용하여 프로토콜 전용 작업을 제한할 수 있습니다. #### 이벤트 기반 흐름 사용자의 언본딩 기간이 완료되면 다음과 같은 일이 발생합니다: 1. **Stable SDK 레이어:** 스테이킹 모듈의 EndBlocker가 언본딩을 완료하고 위임자 주소, 검증자 주소, 금액과 함께 EventTypeCompleteUnbonding을 발생시킵니다. 2. **감지:** x/stable 모듈의 EndBlocker가 스테이킹 이후에 실행되어 블록의 이벤트 로그에서 언본딩 이벤트를 스캔합니다. 각 완료에 대해, 위임자 주소, 검증자 주소, 금액, 블록 높이와 함께 상태에 항목을 큐에 추가합니다. 3. **시스템 TX 생성:** 다음 블록의 PrepareProposal에서, 앱은 큐에 들어 있는 모든 완료를 쿼리합니다. 항목이 존재하면, 현재 블록 높이와 함께 StableSystem.notifyUnbondingCompletions(blockHeight)를 호출하는 시스템 트랜잭션을 생성합니다. 이 트랜잭션은 어떤 사용자 트랜잭션보다도 앞서 블록의 맨 앞에 위치합니다. 4. **실행:** 블록 실행 중에 시스템 트랜잭션이 먼저 실행됩니다. 프리컴파일은 해당 블록 높이에서 큐에 들어 있는 완료를 상태에서 쿼리하고, 각각(최대 100개)에 대해 UnbondingCompleted 이벤트를 발생시키며, 큐에서 삭제합니다. 5. **EVM 가시성:** 이벤트는 트랜잭션 영수증과 로그에 나타나, eth\_getLogs 쿼리, 블록 탐색기, 그리고 StableSystem 프리컴파일을 모니터링하는 모든 애플리케이션에 가시화됩니다. #### 배치 처리 블록이 너무 커지는 것을 방지하기 위해, 시스템은 블록당 최대 100개의 언본딩 완료를 처리합니다. 150개의 완료가 큐에 들어 있으면: * 블록 N: 완료 0-99를 처리하는 시스템 트랜잭션 생성 * 블록 N+1: 완료 100-149를 처리하는 시스템 트랜잭션 생성 프리컴파일은 calldata로 완료 데이터를 받는 대신 상태를 직접 쿼리합니다. 이렇게 하면 트랜잭션 크기를 예측 가능하게 유지하고 비싼 calldata에서 더 저렴한 상태 읽기로 데이터를 옮길 수 있습니다. ### 사용 예제 가장 일반적인 사용 사례는 사용자의 언본딩 기간이 완료될 때 사용자에게 알려야 하는 스테이킹 대시보드입니다. 다음은 언본딩 완료에 대한 리스너를 설정하는 방법입니다. ```javascript import { ethers } from 'ethers'; // StableSystem precompile address const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999'; // ABI for the UnbondingCompleted event const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; // Connect to the Stable network const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz'); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); // Subscribe to all unbonding completions stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => { console.log('Unbonding completed!'); console.log('Delegator:', delegator); console.log('Validator:', validator); console.log('Amount:', ethers.formatEther(amount), 'tokens'); console.log('Block:', event.log.blockNumber); console.log('Tx Hash:', event.log.transactionHash); }); ``` 이 리스너는 어떤 사용자의 언본딩이 완료될 때마다 실행됩니다. 프로덕션 dApp의 경우, 아래와 같이 특정 사용자에 대한 이벤트를 필터링하세요. #### 특정 사용자에 대한 이벤트 필터링 특정 위임자 주소에 대한 이벤트만 받으려면, 인덱싱된 이벤트 매개변수를 사용하여 필터를 생성하세요: ```javascript // Only watch unbondings for a specific user const userAddress = '0xabcd...'; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { // This only fires for the specified user's unbondings showNotification(`Your unbonding of ${ethers.formatEther(amount)} tokens completed!`); refreshUserBalance(userAddress); }); ``` 검증자별 대시보드를 구축하는 경우 검증자로도 필터링할 수 있습니다: ```javascript // Watch all unbondings from a specific validator const validatorAddress = '0x1234...'; const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### 과거 이벤트 쿼리 dApp이 과거의 언본딩 완료 내역을 표시해야 하는 경우, 블록 범위와 함께 이벤트 필터를 사용하여 과거 이벤트를 쿼리할 수 있습니다: ```javascript // Get all unbondings for a user in the last 1000 blocks const currentBlock = await provider.getBlockNumber(); const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter( filter, currentBlock - 1000, currentBlock ); const unbondingHistory = events.map(event => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash })); console.log('Recent unbondings:', unbondingHistory); ``` ### 통합 가이드 #### 1단계: Stable System 컨트랙트 인터페이스 추가 먼저, 프로젝트에 StableSystem 프리컴파일 인터페이스를 추가합니다. Foundry나 Hardhat을 사용하는 경우, 새 인터페이스 파일을 생성하세요: ```solidity interface IStableSystem { event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); } ``` Solidity 컨트랙트 없이 순수 프런트엔드 dApp을 구축하는 경우, 이벤트에 대한 ABI 조각만 있으면 됩니다: ```javascript const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; ``` #### 2단계: 이벤트 리스너 설정 ethers.js 프로바이더를 초기화하고 StableSystem 프리컴파일 주소를 가리키는 컨트랙트 인스턴스를 생성합니다. 프리컴파일은 Stable Testnet과 Stable Mainnet 모두에서 항상 `0x00000000000....0000009999`에 배포됩니다. *참고: 프리컴파일은 아직 Stable Mainnet에 배포되지 않았으며, v1.2.0 업그레이드 이후에 제공될 예정입니다.* ```javascript const provider = new ethers.JsonRpcProvider(RPC_URL); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); ``` #### 3단계: 애플리케이션 로직에서 이벤트 처리 이벤트를 구독하고 그에 따라 애플리케이션 상태를 업데이트합니다. 일반적인 패턴은 다음과 같습니다: * **잔액 업데이트**: 언본딩이 완료되면 사용자의 토큰 잔액을 새로 고칩니다 * **알림 시스템**: 사용자의 언본딩이 완료되면 토스트 알림을 표시합니다 * **대시보드 통계**: 스테이킹 메트릭과 차트를 실시간으로 업데이트합니다 * **트랜잭션 내역**: 완료된 언본딩을 사용자의 활동 피드에 추가합니다 #### 4단계: 연결 문제 처리 이벤트 구독은 지속적인 웹소켓 연결에 의존하므로, 프로덕션 dApp을 위한 재연결 로직을 구현하세요: ```javascript let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function setupEventListener() { const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz'); provider.on('error', (error) => { console.error('Provider error:', error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); stableSystem.on('UnbondingCompleted', handleUnbonding); } ``` ### 왜 이 접근 방식인가? #### 커스텀 인덱서와의 비교 이전에는 Stable SDK에서 SDK 이벤트를 감시하고 데이터베이스에 저장하는 커스텀 인덱서를 실행해야 했습니다. 이는 운영 부담을 가중시키고 잠재적인 장애 지점을 유발합니다. 시스템 트랜잭션을 사용하면 별도의 인덱서 인프라가 필요하지 않습니다. 이벤트는 모든 RPC 노드가 이미 인덱싱하고 제공하는 EVM의 로그 시스템을 통해 기본적으로 사용할 수 있습니다. 어떤 표준 web3 라이브러리든 추가 도구 없이 이러한 이벤트를 구독할 수 있습니다. #### SDK 엔드포인트 폴링과의 비교 시스템 트랜잭션이 없으면, EVM dApp은 언본딩 기간이 완료되었는지 확인하기 위해 주기적으로 Stable SDK REST 엔드포인트를 호출해야 합니다. 이로 인해 여러 문제가 발생합니다: * **증가한 지연 시간**: 5-10초의 폴링 간격은 사용자가 업데이트를 보기까지 그만큼 기다려야 함을 의미합니다 * **높은 부하**: 엔드포인트를 폴링하는 모든 dApp 인스턴스가 RPC 인프라의 부하를 증가시킵니다 * **복잡성**: dApp은 web3 프로바이더(EVM 상호작용용)와 Stable SDK REST 클라이언트(SDK 쿼리용)를 모두 처리해야 합니다 * **실시간 업데이트 부재**: 폴링은 본질적으로 즉각적인 알림을 제공할 수 없습니다 시스템 트랜잭션은 dApp이 EVM 상호작용을 위해 이미 사용하고 있는 동일한 웹소켓 연결을 통해 실시간 이벤트 알림을 제공합니다. 이는 개발자 경험을 단순화하고 인프라 비용을 줄입니다. ### 보안 보장 #### 신뢰가 필요 없는 이벤트 발생 시스템 트랜잭션은 검증자만 실행할 수 있는 `PrepareProposal` ABCI 단계에서 생성됩니다. 사용자가 제출한 트랜잭션은 시스템 발신자 주소(`0x8888888888888888888888888888888888888888`)를 위조할 수 없습니다. EVM의 상태 전이 로직은 StableSystem 프리컴파일 주소로의 트랜잭션만 서명 검증을 건너뛸 수 있도록 강제합니다. 이는 다음을 의미합니다: * 사용자는 언본딩 완료 이벤트를 위조할 수 없습니다 * 사용자는 자신의 트랜잭션에서 `notifyUnbondingCompletions`를 호출할 수 없습니다 * `UnbondingCompleted` 이벤트를 발생시키는 유일한 방법은 Stable SDK 스테이킹 모듈에서 실제 언본딩이 완료되는 것입니다 #### 추가적인 신뢰 가정 없음 시스템 트랜잭션은 블록체인 합의에 이미 필요한 것 이상의 새로운 보안 가정을 도입하지 않습니다. 검증자가 블록을 올바르게 실행한다고 신뢰한다면, 시스템 트랜잭션 이벤트가 Stable SDK 상태 변경을 정확하게 반영한다고 신뢰할 수 있습니다. 이벤트 발생 과정은 결정론적입니다: `EndBlock`에서 동일한 SDK 이벤트가 주어지면, 모든 정직한 검증자는 `PrepareProposal` 동안 동일한 시스템 트랜잭션을 생성합니다. 합의 메커니즘은 검증자가 어떤 시스템 트랜잭션을 포함할지 동의하도록 보장합니다. #### 블록 최종성 Stable 블록체인은 StableBFT의 합의 메커니즘을 통한 빠른 최종성을 사용합니다. 블록이 커밋되면 즉시 최종 확정되며 재구성될 수 없습니다. 이는 일단 `UnbondingCompleted` 이벤트를 받으면 그것이 영구적이라고 신뢰할 수 있음을 의미합니다. 확률적 최종성 체인처럼 여러 번의 확인을 기다릴 필요가 없습니다. dApp은 이벤트를 받는 즉시 사용자 잔액을 업데이트하고 알림을 표시할 수 있습니다. ### 성능 및 제한 사항 #### 배치 크기 제약 각 블록은 시스템 트랜잭션을 통해 최대 100개의 언본딩 완료를 처리합니다. 이 제한은 언본딩 활동이 많은 기간 동안 무제한적인 블록 크기를 방지하기 위해 존재합니다. 실제로 블록당 100개의 완료는 평균 블록 시간 0.7초를 가정할 때 분당 약 9000개의 완료 처리량을 제공합니다. 정상적인 스테이킹 활동은 이 한계에 거의 도달하지 않습니다. 예외적인 상황에서는 완료가 완전히 처리되기 전에 여러 블록 동안 큐에 대기할 수 있습니다. #### 가스 소비 시스템 트랜잭션은 실행 중에 가스를 소비하며, 이는 블록의 가스 한도에 포함됩니다. 가스 비용은 처리되는 완료 수에 따라 선형적으로 증가합니다: * 기본 함수 호출: \~21,000 가스 * 이벤트 발생당: \~3,000 가스 * 상태 읽기: 완료당 \~2,000 가스 100개의 완료로 구성된 전체 배치는 약 521,000 가스를 소비합니다. Stable의 블록 가스 한도는 100,000,000이므로, 이는 사용 가능한 블록 공간의 0.6% 미만에 해당합니다. #### 알림 지연 시간 블록 N 동안 언본딩 기간이 완료되면: 1. Stable 모듈의 `EndBlock`이 블록 N의 상태에 완료를 큐에 추가합니다 2. 블록 N+1의 `PrepareProposal`이 시스템 트랜잭션을 생성합니다 3. 시스템 트랜잭션이 블록 N+1 동안 실행되어 이벤트를 발생시킵니다 이는 언본딩 완료와 EVM 이벤트 발생 사이에 한 블록의 지연(약 0.7초)이 있음을 의미합니다. 언본딩 기간 자체가 7일이므로 대부분의 사용 사례에서 이 지연 시간은 허용 가능합니다. #### 높은 부하 시나리오 언본딩 완료가 블록당 100개보다 빠르게 도착하면, 큐에 누적됩니다. 큐는 FIFO 순서로 처리되므로, 가장 오래된 완료가 항상 먼저 알림됩니다. 지속적인 높은 부하 동안 큐는 일시적으로 늘어날 수 있습니다. 하지만 급증이 가라앉으면, 완료가 더 적은 후속 블록들이 점차 큐를 비웁니다. 시스템은 이벤트를 누락하지 않고 급증을 처리하도록 설계되었습니다. ### 향후 확장 시스템 트랜잭션 메커니즘은 모든 Stable SDK 작업을 EVM 이벤트 공간으로 연결하는 일반적인 패턴을 제공합니다. 현재는 언본딩 완료에만 사용되지만, 이 아키텍처는 추가적인 사용 사례를 다루도록 확장할 수 있습니다: #### 스테이킹 작업 언본딩 외에도, 다른 스테이킹 이벤트가 EVM 알림을 발생시킬 수 있습니다: * 검증자에 의한 수수료율 변경 * 검증자 수감 및 수감 해제 #### 거버넌스 실행 거버넌스 제안이 통과되어 실행될 때, 시스템 트랜잭션은 제안 ID와 실행 결과와 함께 이벤트를 발생시킬 수 있습니다. 이를 통해 dApp은 거버넌스 모듈을 폴링하지 않고도 매개변수 변경이나 업그레이드에 반응할 수 있습니다. #### 일반 이벤트 브리지 이 패턴은 각 모듈이 어떤 SDK 이벤트를 EVM에 미러링할지 등록하는 구성 가능한 이벤트 브리지로 일반화될 수 있습니다. 이는 모듈별 커스텀 로직 없이도 모든 Stable SDK 작업에 대한 포괄적인 가시성을 제공합니다. 핵심 아키텍처 원칙은 시스템 트랜잭션이 블록 제안 동안 검증자에 의해서만 생성되는 프로토콜 수준 기능으로 유지된다는 것입니다. ## 생태계 이 문서에서는 브릿지(LayerZero) 및 USDT0에 대한 정보를 확인할 수 있습니다. ### Stable Testnet의 LayerZero | 이름 | 값 | | ----------------- | ------------------------------------------ | | eid | 40374 | | chainKey | stable-testnet | | stage | testnet | | endpointV2View | 0x6Ac7bdc07A0583A362F1497252872AE6c0A5F5B8 | | endpointV2 | 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 | | sendUln302 | 0x9eCf72299027e8AeFee5DC5351D6d92294F46d2b | | receiveUln302 | 0xB0487596a0B62D1A71D0C33294bd6eB635Fc6B09 | | blockedMessageLib | 0xa229b65cc2191bf60bc24efcda3487d7b5c0c9f0 | | executor | 0x701f3927871EfcEa1235dB722f9E608aE120d243 | | deadDVN | 0xC1868e054425D378095A003EcbA3823a5D0135C9 | ### Stable Testnet의 USDT0 | 이름 | 값 | | ----------- | ------------------------------------------ | | wrapper | 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb | | composer | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D | | OFT | 0x779Ded0c9e1022225f8E0630b35a9b54bE713736 | | USDT0 impl | 0x3f9E27457ac494fC729beB50e6af04Ec34e3828E | | USDT0 proxy | 0x78Cf24370174180738C5B8E352B6D14c83a6c9A9 | ### Sepolia OFT 컨트랙트 및 USDT0 컨트랙트 (참고용) | 이름 | 값 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sepolia OFT | [https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126](https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126) | | Sepolia USDT | [https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract](https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) | ## 테스트넷 정보 Stable 테스트넷에 접속하기 위해 알아야 할 모든 것입니다. ### 네트워크 개요 | 구성 항목 | 값 | | ------------ | -------------- | | **네트워크 이름** | Stable Testnet | | **Chain ID** | `2201` | | **가스 토큰** | USDT0 | | **거버넌스 토큰** | STABLE | | **블록 타임** | 약 0.7초 | ### 블록 익스플로러 | 익스플로러 | URL | | -------------- | ---------------------------------------------------------------- | | **Stablescan** | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) | ### RPC 엔드포인트 #### 기본 엔드포인트 | 유형 | 엔드포인트 | 용도 | | ---------------- | ---------------------------------------------------------------- | -------- | | **EVM JSON-RPC** | [https://rpc.testnet.stable.xyz](https://rpc.testnet.stable.xyz) | EVM 트랜잭션 | | **WebSocket** | wss\://rpc.testnet.stable.xyz | 실시간 업데이트 | :::note 공개 RPC 엔드포인트는 **IP당 10초마다 1,000건의 요청**으로 속도가 제한됩니다. 한도를 초과한 요청은 `HTTP 429`를 반환합니다. 더 높은 처리량이 필요하면 [서드파티 RPC 제공자](/ko/reference/rpc-providers)를 사용하세요. ::: ### 체인 정보 | 매개변수 | EVM | | ------------ | ------- | | **Chain ID** | `2201` | | **주소 형식** | `0x...` | | **가스 토큰** | `USDT0` | | **소수 자릿수** | 18 | ### 포셋 & 도구 | 도구 | URL | 설명 | | -------------- | ------------------------------------------------------ | --------- | | **포셋(Faucet)** | [https://faucet.stable.xyz](https://faucet.stable.xyz) | 테스트 토큰 받기 | | **스냅샷** | [노드 운영자 가이드](/ko/how-to/use-node-snapshots) 참고 | 체인 스냅샷 | ## 버전 기록 Stable 테스트넷의 전체 버전 기록 및 관련 문서입니다. ### 현재 버전 정보 * **현재 버전**: `v1.4.0-rc0` * **다음 업그레이드**: `TBD` * **업그레이드 높이**: `TBD` * **예상 시간**: `TBD` ### 버전 기록 #### 현재 및 이전 버전 | 버전 | 커밋 | 업그레이드 높이 | 바이너리 | 상태 | | -------------- | ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | **v1.4.0-rc0** | `83b5efb` | 57,806,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.4.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.4.0-rc0-linux-arm64-testnet.tar.gz) | 현재 | | **v1.3.1-rc0** | `75bb546` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.1-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.1-rc0-linux-arm64-testnet.tar.gz) | | | **v1.3.0-rc1** | `25b5e47` | 53,265,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc1-linux-arm64-testnet.tar.gz) | | | **v1.3.0-rc0** | `864d54c` | 49,855,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-arm64-testnet.tar.gz) | | | **v1.2.2-rc0** | `8bd5d5e` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-arm64-testnet.tar.gz) | | | **v1.2.1-rc1** | `7ff9a8a` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-arm64-testnet.tar.gz) | | | **v1.2.0-rc1** | `263c033` | 41,306,450 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz) | | | **v1.2.0** | `ee8ca35` | 40,392,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-arm64-testnet.tar.gz) | | | **v1.1.2** | `3d83aa3` | 34,649,300 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-arm64-testnet.tar.gz) | | | **v1.1.1** | `8becd6b` | 33,152,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-arm64-testnet.tar.gz) | | | **v1.1.0** | `17ceaa7` | 32,309,700 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-arm64-testnet.tar.gz) | | | **v0.8.1** | `1eb65d5` | 30,770,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-arm64-testnet.tar.gz) | | | **v0.8.0** | `e55efb6` | 29,410,999 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-arm64-testnet.tar.gz) | Bank 프리컴파일 개선 | | **v0.7.2** | `3c53e14` | 27,258,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-arm64-testnet.tar.gz) | StableBFT 통합 | | **v0.6.0** | `5cc1ad6` | 19,587,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.6.0-linux-amd64-testnet.tar.gz) | 사소한 수정 | | **v0.5.0** | `919281d` | 18,719,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.5.0-linux-amd64-testnet.tar.gz) | 사소한 수정 | | **v0.4.0** | `c6240c0` | 18,666,150 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.4.0-linux-amd64-testnet.tar.gz) | Stable SDK v0.53.4 | | **v0.3.0** | `a4f5ac5` | 9,166,131 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.3.0-linux-amd64-testnet.tar.gz) | EVM 가치 전송 허용 목록 | | **v0.2.1** | `53e6e073` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.1-linux-amd64-testnet.tar.gz) | 비호환성 없는 업데이트 | | **v0.2.0** | `8bdd771` | 8,956,584 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.0-linux-amd64-testnet.tar.gz) | 기능 업데이트 | | **v0.1.0** | `10dfg542` | 제네시스 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.1.0-linux-amd64-testnet.tar.gz) | 제네시스 (2025-04-07) | ### 관련 문서 * [업그레이드 가이드](/ko/how-to/upgrade-node) - 단계별 업그레이드 절차 * [테스트넷 정보](/ko/reference/testnet-information) - 현재 네트워크 세부 정보 ## 토크노믹스 Stable은 스테이블코인 결제, 엔터프라이즈급 결제 및 USDT 중심 인프라에 최적화된 고성능 레이어 1 블록체인입니다. 이 토큰노믹스 페이지는 STABLE 토큰의 공급, 분배 및 경제 설계를 설명합니다. *** ### 개요 | 항목 | 세부사항 | | :-------- | :---------------------- | | **심볼** | STABLE | | **총 공급량** | 100,000,000,000 토큰 | | **표준** | ERC-20 (Stable 메인넷 EVM) | | **소수점** | 18 | STABLE은 Stable 메인넷과 생태계의 거버넌스 토큰으로, 검증자, 개발자 및 사용자 간의 장기적인 경제적 정렬을 지원하도록 설계되었습니다. *** ### 토큰 분배 **총 공급량:** 100,000,000,000 STABLE 토큰 | 카테고리 | 할당 | STABLE 수량 | | :------------- | :--- | :-------------- | | **투자자 및 자문위원** | 25% | 25,000,000,000 | | **팀** | 25% | 25,000,000,000 | | **생태계 및 커뮤니티** | 40% | 40,000,000,000 | | **제네시스 분배** | 10% | 10,000,000,000 | | **총합** | 100% | 100,000,000,000 | *** ### 발행 모델 및 공급 일정 * 총 공급량은 100,000,000,000 STABLE 토큰으로 고정되어 있습니다. * Stable 메인넷 출시 시 공급량의 일부만 유통됩니다. * 팀 및 투자자와 자문위원 할당은 장기적 헌신을 보장하기 위해 1년 클리프가 있는 4년 선형 베스팅 모델을 적용받습니다. *** ### 할당 #### 제네시스 분배 - 총 토큰 공급량의 10% 초기 부트스트래핑과 시장 유동성 공급, 에어드롭, 초기 지지자와 거래소 및 생태계 파트너와의 캠페인 보상을 위해 설계되었습니다. **베스팅 일정** * Stable 메인넷 출시 시 100% 잠금 해제 #### 생태계 및 커뮤니티 - 총 토큰 공급량의 40% 장기적인 생태계 및 커뮤니티 성장을 지원합니다: * Stable 소프트웨어 및 생태계 개발 지원 * 개발자 보조금 * 사용자 온보딩 인센티브 * 결제 파트너 통합 * 온체인 활동 보상 * 해커톤, 앰배서더 프로그램 * 인프라 보조금 **베스팅 일정** * **초기 잠금 해제:** 전략적 출시 파트너와의 인센티브, 유동성 수요 및 초기 생태계 성장 캠페인 구현을 위해 Stable 메인넷 출시 시 총 공급량의 8%가 잠금 해제됩니다. * **총 베스팅 기간:** 총 공급량의 32%에 대해 이후 3년 선형 베스팅 #### 팀 - 총 토큰 공급량의 25% * 창립 팀원, 엔지니어, 연구원 및 기여자에게 할당되었습니다 * 팀과 Stable 생태계 간의 장기적인 정렬을 보장하도록 설계되었습니다. **베스팅 일정** * **1년 클리프:** 첫 12개월 동안 토큰이 잠금 해제되지 않습니다 * **총 베스팅 기간:** Stable 메인넷 출시부터 48개월 선형 베스팅 #### 투자자 및 자문위원 - 총 토큰 공급량의 25% 자금 조달 라운드 및 자문 지원을 위해 할당되었습니다. **베스팅 일정** * **1년 클리프:** 첫 12개월 동안 토큰이 잠금 해제되지 않습니다 * **총 베스팅 기간:** Stable 메인넷 출시부터 48개월 선형 베스팅 *** ### 발행 차트 STABLE 토큰 발행 차트 STABLE 토큰 발행 차트 *** ### 경제 설계 원칙 Stable의 토큰 경제학은 세 가지 기본 목표를 중심으로 설계되었습니다: #### 1. 결제에 최적화된 레이어 1 구동 STABLE 토큰은 고처리량, 저지연 인프라를 인센티브화하여 1초 미만의 블록 확인 및 기업급 결제 보장을 지원합니다. #### 2. 지속 가능한 생태계 성장 지원 총 토큰 공급량의 40%가 주요 개발 및 성장 영역에 중점을 두고 장기적 성장에 전념합니다 * 개발자 보조금 * 파트너 통합 * 새로운 생태계 애플리케이션 #### 3. 베스팅을 통한 장기 기여자 정렬 팀 할당은 1년 클리프가 있는 4년 선형 베스팅 모델을 사용하여 네트워크 개발에 대한 장기적인 정렬과 지속적인 기여를 보장합니다. *** ### STABLE 토큰의 유틸리티 STABLE 토큰은 Stable 메인넷의 ERC-20 거버넌스 토큰입니다. 다음과 같은 용도로 사용할 수 있습니다: * 검증자 선출 * 프로토콜 업그레이드 투표 * 거버넌스 제안 처리 * 검증자로부터 가스 수수료 분배를 받기 위한 자격 증명 역할 Stable 네트워크에서 모든 트랜잭션은 네이티브 가스 토큰으로 USDT0을 사용합니다. 이러한 USDT0 가스 수수료는 스마트 컨트랙트가 관리하는 재무에 수집됩니다. 토큰 보유자가 STABLE 토큰을 검증자에게 스테이킹하면 검증자는 재무에서 가스 수수료를 스테이커에게 비례적으로 분배할 수 있습니다. ## 지갑 ### 지갑 개요 표 | **제공업체** | **카테고리** | **보안 방식** | **문서 / 시작하기** | **참고** | | :----------------------------------------------------------------- | :------------------ | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Stable Pay | 사용자 지갑 | TSS-MPC 기반 자기수탁 | [https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain](https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain) | Stable 위에 구축된 USDT 네이티브 결제 지갑; 즉시 전송에 최적화 | | [Wallet Development Kit by Tether (WDK)](https://wallet.tether.io) | 지갑 SDK | 자기수탁 | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | 멀티체인에서 자기수탁 지갑을 구축하기 위한 Tether의 오픈소스 SDK | | [Binance Wallet](https://www.binance.com/en/web3wallet) | 사용자 지갑 | MPC 기반 자기수탁 / 반수탁 지갑 | [https://developers.binance.com/docs/binance-spot-api-docs/README](https://developers.binance.com/docs/binance-spot-api-docs/README) | 멀티체인 지갑, Stable USDT 지원 | | [Reown](https://reown.com/) (구 WalletConnect / WalletKit) | 연결 / 지갑 인프라 | 프로토콜 수준 서명 및 보안 릴레이 | [https://docs.reown.com/appkit/overview](https://docs.reown.com/appkit/overview) | 600개 이상의 지갑 지원, 멀티체인, SDK 기반 통합, 임베디드 지갑 흐름에 이상적 | | [Bitget Wallet](https://web3.bitget.com/en) | 사용자 지갑 | 비수탁 지갑 (개인 키는 사용자가 관리) | [https://web3.bitget.com/en/docs/](https://web3.bitget.com/en/docs/) | 내장 dApp 브라우저; 멀티 자산 및 멀티체인 지원 | | [Gate Wallet](https://www.gate.com/) (Gate Onchain) | 사용자 지갑 | 거래소 연동 지갑 | [https://www.gate.com/](https://www.gate.com/) | 거래소 연동 지갑; CEX ↔ 지갑 흐름에 적합 | | [OKX Wallet](https://web3.okx.com/) (OKX Onchain) | 사용자 지갑 | 비수탁 / 복구를 위한 MPC | [https://www.okx.com/earn/onchain-earn](https://www.okx.com/earn/onchain-earn) | 거래소 통합이 가능한 멀티체인 지갑 | | [Anchorage](https://www.anchorage.com/) | 수탁 / 기관 지갑 | 은행급 규제 수탁 (연방 인가 은행) | [https://www.anchorage.com/who-we-serve](https://www.anchorage.com/who-we-serve) | stable 자산을 위한 기관급 수탁 | | [Dynamic](https://www.dynamic.xyz/) | 임베디드 / 인앱 지갑 인프라 | SDK 또는 백엔드를 통한 관리형 키 / 수탁 인프라 | [https://www.dynamic.xyz/docs/introduction/welcome](https://www.dynamic.xyz/docs/introduction/welcome) | 외부 지갑 없이 앱에 지갑 흐름을 임베드할 수 있게 함 | | [Alchemy](https://www.alchemy.com/) | 스마트 지갑 및 계정 추상화 인프라 | Bundler + Paymaster 인프라 (ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | AA 지갑을 구동; 스폰서드 가스, 스마트 계정 지원 | | [Atomic Wallet](https://atomicwallet.io/) | 사용자 지갑 | 비수탁 (키가 사용자 기기에 저장) | [https://atomicwallet.io/assets-status](https://atomicwallet.io/assets-status) | 멀티 자산 모바일, 데스크톱, 브라우저 확장 지갑; 내장 [Simplex](/ko/reference/ramps#simplex-by-nuvei) 온램프를 통해 Stable에서 USDT 구매 | ### 카테고리 가이드 * **사용자 지갑:** 모바일 앱, 브라우저 확장 프로그램, 거래소 연동 지갑과 같은 전통적인 소비자용 지갑입니다. 사용자가 USDT를 보관하고, 전송하고, dApp에 연결하며, Stable과 직접 상호작용할 수 있게 합니다. * **지갑 SDK:** 개발자에게 사전 구축된 도구, API, 인프라를 제공하여 지갑 생성, 키 관리, 트랜잭션 서명, 블록체인 상호작용을 애플리케이션에 직접 통합할 수 있게 하는 소프트웨어 개발 키트입니다. * **수탁 / 기관 지갑:** 기관을 위한 규제되고 엔터프라이즈급 자산 수탁을 제공하는 플랫폼입니다. 이러한 솔루션은 최종 사용자 흐름보다는 컴플라이언스, 거버넌스 통제, 보안 키 관리, 자금 운용에 중점을 둡니다. * **임베디드 / 인앱 지갑:** SDK 또는 백엔드 시스템을 통해 애플리케이션 내부에서 생성되는 지갑입니다. 외부 암호화폐 지갑을 설치하거나 이해할 필요 없이 일반 사용자에게 매끄러운 온보딩을 가능하게 합니다. * **스마트 지갑 / 계정 추상화:** 가스리스 트랜잭션, 묶음 작업, 자동화된 실행과 같은 사용자 정의 로직을 지원하는 프로그래밍 가능한 지갑입니다. 개발자가 정의한 동작으로 기본 지갑 기능을 확장합니다. * **MPC 지갑 제공업체:** 멀티파티 연산(MPC)을 사용하여 개인 키 제어를 여러 당사자 또는 기기에 분산하는 키 관리 시스템입니다. 전통적인 시드 문구 없이 높은 보안 수탁이 필요한 앱이나 기업에 이상적입니다. * **연결 제공업체:** 지갑과 dApp을 연결하는 WalletConnect(Reown)와 같은 프로토콜입니다. 이들은 자산을 저장하거나 지갑 자체로 작동하지 않으며, 대신 트랜잭션 서명 및 상호작용을 위한 보안 통신 채널을 제공합니다. ### 1. 사용자 지갑 이들은 주요 글로벌 거래소가 제공하는 최종 사용자 지갑입니다. 사용자가 USDT를 보관하고, 자금을 전송하며, Stable의 애플리케이션에 연결할 수 있게 합니다. #### Stable Pay 빠르고 스테이블코인 네이티브 트랜잭션을 위해 설계된, Stable 위에 구축된 비수탁 결제 지갑입니다. Stable Pay는 즉시 USDT 결제, 예측 가능한 수수료, 일상적인 전송 및 상거래에 최적화된 간단한 사용자 경험을 제공합니다. **기능** * Stable을 위한 비수탁 지갑 * 즉시 USDT 결제 * 예측 가능하고 일관된 트랜잭션 비용 * Stable의 USDT 네이티브 정산 계층 위에 직접 구축 * 결제 및 상거래를 위해 설계된 소비자 친화적 UI #### Binance Wallet 거래량 기준 세계 최대 거래소와 통합된 널리 사용되는 멀티체인 지갑입니다. **기능** * Stable USDT 지원 * Binance 생태계와 직접 통합 * 모바일 및 확장 지갑 옵션 #### Bitget Wallet 암호화폐, 주식, ETF를 지원하며 Bitget 거래소 생태계에 연결된 멀티 자산 지갑입니다. **기능** * Stable USDT 지원 * 내장 dApp 브라우저 * Bitget 거래 계정과의 매끄러운 통합 #### Gate Wallet (Gate Onchain) 세계 최대 현물 거래소 중 하나가 지원하는 지갑 제품입니다. **기능** * Stable USDT 지원 * Gate 거래소와 지갑 간의 간편한 전송 * dApp 및 웹앱 연결성 #### OKX Wallet (OKX Onchain) 전 세계적으로 사용되는 강력한 멀티체인 지갑입니다. **기능** * Stable USDT 지원 * 깊은 OKX 생태계 통합 * 웹, 모바일, 확장 지갑 옵션 #### Atomic Wallet 데스크톱, 모바일, 브라우저 확장 프로그램으로 제공되는 비수탁 멀티 자산 지갑입니다. Atomic Wallet은 [Simplex by Nuvei](/ko/reference/ramps#simplex-by-nuvei) 온램프를 통합하여, 사용자가 카드, Apple Pay, Google Pay 또는 은행 송금으로 앱 내에서 직접 Stable에서 USDT를 구매할 수 있습니다. **기능** * Stable에서 USDT 보관, 전송, 수신 * 개인 키는 사용자 기기에 로컬로 저장 * Simplex를 통한 Stable에서의 USDT 인앱 구매 흐름 * Windows, macOS, Linux, iOS, Android, Chrome에서 사용 가능 **시작하기**: [atomicwallet.io](https://atomicwallet.io/downloads)에서 Atomic Wallet을 다운로드하고, Stable을 네트워크로 추가한 다음, 인앱 구매 흐름을 사용하여 Simplex를 통해 Stable에서 USDT를 구매하세요. ### 2. 지갑 SDK #### Development Kit by Tether (WDK) 모든 플랫폼과 블록체인에서 자기수탁 지갑을 구축하기 위한 Tether의 오픈소스 SDK입니다. **기능** * 멀티체인 지원: Bitcoin, Ethereum, TON, TRON, Solana, Spark 등 * 에이전트 지갑: Stable에서 AI 에이전트 지갑 및 x402 결제에 대한 네이티브 지원 * DeFi 통합: 스왑, 브리지, 대출 프로토콜을 위한 플러그인 지원 * 확장 가능한 설계: 새로운 블록체인이나 프로토콜을 위한 사용자 정의 모듈 추가 **시작하기**: [`@tetherto/wdk`](https://www.npmjs.com/package/@tetherto/wdk)와 [`@tetherto/wdk-wallet-evm`](https://www.npmjs.com/package/@tetherto/wdk-wallet-evm)를 설치한 다음, [WDK 문서](https://docs.wallet.tether.io)를 따라 Stable을 대상 체인으로 구성하세요. ### 3. 수탁 및 기관 지갑 #### Anchorage 디지털 자산을 위한 기관급 수탁을 제공하는 연방 인가 국립 은행입니다. **기능** * Stable USDT를 위한 보안 수탁 * 완전한 컴플라이언스 및 규제 감독 * 엔터프라이즈급 키 관리 및 접근 제어 ### 4. 임베디드 / 인앱 지갑 SDK를 통해 애플리케이션에 직접 임베드된 지갑으로, 매끄러운 사용자 온보딩 및 결제 흐름을 가능하게 합니다. #### Dynamic 수천 개의 애플리케이션과 4천만 명 이상의 사용자에게 서비스를 제공하는 엔터프라이즈급 지갑 인프라입니다. **기능** * 지갑 생성 및 인증 * 임베디드 지갑 흐름 * 앱 및 핀테크를 위한 사용자 온보딩 **시작하기**: [Dynamic SDK 설정 문서](https://docs.dynamic.xyz/introduction/welcome)를 따라 SDK를 설치하고 Stable을 앱에서 지원되는 네트워크로 구성하세요. #### Reown (구 WalletConnect) 지갑을 애플리케이션에 연결하기 위한 널리 채택된 표준입니다. **기능** * 보안 지갑-dApp 연결 * 모바일, 데스크톱, 확장 지갑 지원 * 폭넓은 생태계 호환성 **지갑 온보딩을 위한 Reown SDK** Stable은 개발자가 사용자에게 매끄러운 지갑 및 온보딩 경험을 제공할 수 있도록 **Reown SDK**와의 통합을 지원합니다. Reown은 WalletConnect 네트워크의 공식 게이트웨이 역할을 하는 오픈소스 올인원 SDK를 제공합니다. 이를 통해 애플리케이션 내에서 매끄러운 지갑 연결, 트랜잭션, 로그인, 임베디드 지갑(이메일 및 소셜 로그인), 온체인 결제, 인앱 스왑 등을 가능하게 합니다. **시작하기** * Reown 문서를 방문하세요: [https://docs.reown.com/overview](https://docs.reown.com/overview) ### 5. 스마트 지갑 및 계정 추상화 프로그래밍 가능한 지갑, 가스리스 트랜잭션, 지출 규칙, 고급 UX를 가능하게 하는 인프라입니다. #### 개요 표 | **제공업체** | **카테고리** | **보안 방식** | **문서 / 시작하기** | **참고** | | :------------------------------------------ | :------------------------- | :--------------------------------- | :------------------------------------------------------------------------------------- | :------------------------- | | [**Holdstation**](https://holdstation.com/) | 스마트 지갑 (AA) | 스마트 컨트랙트 지갑 + 생체 인증 | [https://docs.holdstation.com/holdstation/](https://docs.holdstation.com/holdstation/) | 가스리스 흐름, DeFi 네이티브 지갑 | | [**Daimo**](https://pay.daimo.com/) | AA 결제 지갑 | 스마트 계정, 시드 문구 없음 | [https://paydocs.daimo.com/](https://paydocs.daimo.com/) | 원클릭 결제, 스테이블코인 우선 | | [**Alchemy**](https://alchemy.com) | AA 인프라 (Bundler/Paymaster) | Bundler + paymaster 인프라 (ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | AA 지갑이 Stable에서 구축할 수 있게 함 | #### Alchemy Alchemy는 스마트 계정을 배포하고, 가스를 스폰서하며, 소비자급 지갑을 구축하는 데 필요한 핵심 AA 인프라와 API를 제공합니다. **기능** * 스마트 계정 SDK 및 API * 가스리스 작업을 위한 Paymaster 지원 * 가스 추상화 도구 * 스마트 지갑 개발자를 위한 확장 가능한 인프라 **시작하기**: [Alchemy 스마트 계정 SDK](https://docs.alchemy.com)를 사용하여 ERC-4337 스마트 계정을 배포하고 Stable에서 스폰서드 가스를 위한 paymaster를 구성하세요. **문서**\ [https://docs.alchemy.com](https://docs.alchemy.com) #### Holdstation 계정 추상화 및 생체 보안 상호작용을 제공하는 스마트 컨트랙트 지갑입니다.\ **기능** * 완전한 AA 지원 스마트 지갑 * 가스리스 트랜잭션 및 스폰서드 수수료 * 생체 인증 및 세션 키 * 통합 거래 및 DeFi 실행 계층 **시작하기**: [Holdstation 개발자 문서](https://docs.holdstation.com/holdstation/)를 탐색하여 스마트 지갑 흐름과 스폰서드 트랜잭션을 애플리케이션에 통합하세요. #### Daimo 즉시 스테이블코인 지출 및 결제를 위해 설계된 소비자급 계정 추상화 지갑입니다.\ **기능** * 스마트 지갑 실행이 가능한 AA 기반 UX * 모든 체인에서 원클릭 결제 * 시드 문구 없음; 보안 키 복구 * 결제 앱 및 스테이블코인 유틸리티에 이상적 **시작하기**: [Daimo Pay 문서](https://paydocs.daimo.com/)를 방문하여 Stable에서 애플리케이션에 원클릭 스테이블코인 결제 흐름을 추가하세요. ### Stable 네트워크 설정 #### 지갑에 Stable 추가하기 **네트워크 파라미터:** * **Network Name:** Stable * **Chain ID:** 988 * **Currency:** USDT0 * **RPC URL:** [https://rpc.stable.xyz](https://rpc.stable.xyz) * **Block Explorer:** [https://stablescan.xyz](https://stablescan.xyz) #### 지갑 통합 구축하기 다음을 통해 Stable 지원을 추가할 수 있습니다: * Stable RPC를 통한 서명 및 가스 추정 활성화 * USDT 네이티브 전송 지원 * dApp을 위한 WalletConnect 통합 * 체인 목록 또는 메타데이터 레지스트리에 Stable 추가 ### Stable을 통합하는 지갑이 있나요? 이 섹션에 등재되려면 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz)로 팀에 연락하실 수 있습니다. ## EIP-7702를 활용한 계정 추상화 이 가이드는 EOA에 EIP-7702를 적용하고 위임을 사용하여 세 가지 패턴, 즉 일괄 결제, 지출 한도, 세션 키를 구현하는 과정을 단계별로 설명합니다. EOA는 그 과정 내내 자신의 주소와 개인 키를 유지합니다. :::note **개념:** EIP-7702가 Stable에서 무엇을 가능하게 하는지와 보안 고려 사항에 대해서는 [EIP-7702](/ko/explanation/eip-7702)를 참조하세요. ::: ### 사전 요구 사항 * EOA와 스마트 컨트랙트 계정의 차이 이해(EOA는 기본적으로 코드가 없습니다). * EVM 트랜잭션 유형에 대한 이해([EIP-2718](https://eips.ethereum.org/EIPS/eip-2718)). ### 개요 EIP-7702는 `authorizationList`를 담는 새로운 트랜잭션 유형(`0x04`)을 도입합니다. 각 권한 부여는 EOA가 해당 트랜잭션에서 실행할 코드를 가진 스마트 컨트랙트를 지정합니다. 흐름은 다음과 같습니다: 1. **위임 컨트랙트 선택 또는 배포**: EOA가 사용할 로직을 구현하는 표준 Solidity 컨트랙트입니다. 배포된 컨트랙트를 사용하거나 직접 배포할 수 있습니다. 가능하면 감사받은 컨트랙트를 사용하세요. 2. **권한 부여 서명**: EOA 소유자가 위임 컨트랙트를 승인하는 메시지에 서명합니다. 3. **EIP-7702 트랜잭션 제출**: 트랜잭션에 권한 부여가 포함되며, EOA는 실행 중에 위임 컨트랙트의 코드를 실행합니다. ### 사용 사례: 일괄 트랜잭션 아래 단계는 `Multicall3`을 위임 컨트랙트로 사용하여 이 흐름을 단계별로 설명합니다. `Multicall3`은 여러 호출을 단일 트랜잭션으로 집계하는 널리 배포된 유틸리티 컨트랙트입니다. `Multicall3`을 EIP-7702 위임 대상으로 지정하면 EOA가 임의의 컨트랙트 상호작용(토큰 전송, 승인, 컨트랙트 읽기, 또는 이들의 조합)을 하나의 원자적 트랜잭션으로 일괄 처리할 수 있습니다. 일괄 결제는 그 한 가지 예입니다. 급여 지급을 위해 열 개의 별도 트랜잭션을 보내는 대신, EOA는 이를 한 번에 모두 실행합니다. #### 1단계: Multicall3을 위임 컨트랙트로 사용 `Multicall3`은 Stable에서 `0xcA11bde05977b3631167028862bE2a173976CA11`에 배포되어 있습니다. 이미 배포되어 널리 사용되고 있으므로 직접 위임 컨트랙트를 배포할 필요가 없습니다. EIP-7702 권한 부여에 서명하면 위임 대상에 EOA에 대한 완전한 실행 권한이 부여됩니다. ```solidity // Multicall3 interface (relevant functions only) interface IMulticall3 { struct Call3 { address target; bool allowFailure; bytes callData; } struct Result { bool success; bytes returnData; } /// @notice Aggregate calls, allowing each to succeed or fail independently function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); } ``` #### 2단계: 권한 부여 서명 EOA 소유자가 위임 컨트랙트를 지정하는 권한 부여에 서명합니다. 이 권한 부여는 EIP-7702 트랜잭션에 포함됩니다. ```javascript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_TESTNET_CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const DELEGATE_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); ``` ```javascript // signAuthorization.ts import { ethers } from "ethers"; import { DELEGATE_ADDRESS, STABLE_TESTNET_CHAIN_ID, provider, wallet } from "./config"; export async function signAuthorization() { const authorization = { chainId: STABLE_TESTNET_CHAIN_ID, address: DELEGATE_ADDRESS, nonce: await provider.getTransactionCount(wallet.address), }; return wallet.signAuthorization(authorization); } ``` #### 3단계: EIP-7702 트랜잭션 제출 권한 부여를 `Multicall3.aggregate3` 호출과 결합합니다. 이 예시는 세 개의 USDT0 전송을 단일 트랜잭션으로 일괄 처리합니다. ```javascript import { ethers } from "ethers"; import { wallet, USDT0_ADDRESS } from "./config"; import { signAuthorization } from "./signAuthorization"; const usdt0Interface = new ethers.Interface([ "function transfer(address to, uint256 amount)", ]); const batchInterface = new ethers.Interface([ "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) returns ((bool success, bytes returnData)[])", ]); async function main() { const recipients = [ { to: "0xAlice...", amount: ethers.parseUnits("100", 6) }, { to: "0xBob...", amount: ethers.parseUnits("200", 6) }, { to: "0xCarol...", amount: ethers.parseUnits("150", 6) }, ]; const batchData = batchInterface.encodeFunctionData("aggregate3", [ recipients.map(({ to, amount }) => ({ target: USDT0_ADDRESS, allowFailure: false, callData: usdt0Interface.encodeFunctionData("transfer", [to, amount]), })), ]); const signedAuth = await signAuthorization(); const tx = await wallet.sendTransaction({ type: 4, // EIP-7702 transaction type to: wallet.address, // call is directed at the EOA itself data: batchData, // aggregate3 call to execute authorizationList: [signedAuth], maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Batch transactions executed in tx:", receipt.hash); } ``` ```text Batch transactions executed in tx: 0x... ``` EOA는 `Multicall3.aggregate3`을 통해 세 개의 호출을 모두 단일 원자적 트랜잭션으로 실행합니다. 위임은 명시적으로 변경되거나 해제될 때까지 지속됩니다. 이 예시는 일괄 결제를 보여주지만, 동일한 패턴이 컨트랙트 호출의 모든 조합에 적용됩니다. ### 사용 사례: 지출 한도 위임 컨트랙트는 계정 마이그레이션 없이 EOA에 트랜잭션당 또는 일일 상한을 적용할 수 있습니다. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title SpendingLimitExecutor /// @notice Delegate contract that enforces daily spending caps contract SpendingLimitExecutor { mapping(address => uint256) public dailyLimit; mapping(address => uint256) public spentToday; mapping(address => uint256) public lastResetDay; function setDailyLimit(uint256 limit) external { dailyLimit[msg.sender] = limit; } function executeWithLimit( address target, uint256 value, bytes calldata data ) external payable { uint256 today = block.timestamp / 1 days; if (today > lastResetDay[msg.sender]) { spentToday[msg.sender] = 0; lastResetDay[msg.sender] = today; } spentToday[msg.sender] += value; require( spentToday[msg.sender] <= dailyLimit[msg.sender], "daily limit exceeded" ); (bool success,) = target.call{value: value}(data); require(success, "call failed"); } } ``` ### 사용 사례: 세션 키 세션 키를 사용하면 dApp이 범위가 지정된 권한(시간 창과 허용된 대상 컨트랙트 집합) 내에서 EOA를 대신하여 트랜잭션을 실행할 수 있습니다. 이는 빈번한 온체인 상호작용이 그렇지 않으면 반복적인 지갑 승인을 요구하게 되는 dApp에 유용합니다. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title SessionKeyExecutor /// @notice Delegate contract that grants scoped, time-limited access to a session key contract SessionKeyExecutor { struct Session { address key; uint256 validUntil; uint256 spendingLimit; uint256 spent; } mapping(address => Session) public sessions; mapping(address => mapping(address => bool)) public allowedTargets; /// @notice Register a session key with scoped permissions function startSession( address key, uint256 validUntil, uint256 spendingLimit, address[] calldata targets ) external { sessions[msg.sender] = Session({ key: key, validUntil: validUntil, spendingLimit: spendingLimit, spent: 0 }); for (uint256 i = 0; i < targets.length; i++) { allowedTargets[msg.sender][targets[i]] = true; } } /// @notice Execute a call using the session key function executeAsSessionKey( address owner, address target, uint256 value, bytes calldata data ) external { Session storage session = sessions[owner]; require(msg.sender == session.key, "not session key"); require(block.timestamp <= session.validUntil, "session expired"); require(allowedTargets[owner][target], "target not allowed"); uint256 beforeBalance = owner.balance; (bool success,) = target.call{value: value}(data); require(success, "call failed"); session.spent += owner.balance - beforeBalance; require(session.spent <= session.spendingLimit, "budget exceeded"); } /// @notice Revoke the active session function revokeSession() external { delete sessions[msg.sender]; } } ``` ### 중요 고려 사항 * **지속적인 위임**: 위임은 EOA가 명시적으로 변경하거나 해제할 때까지 지속됩니다. 단일 트랜잭션에 국한되지 않습니다. * **가스 비용**: EIP-7702 트랜잭션은 권한 부여 처리로 인해 기본 가스가 약간 더 높지만, 위임이 여러 호출을 일괄 처리할 때 상쇄됩니다. * **감사받은 위임 사용**: 악의적인 위임 컨트랙트는 EOA의 자산을 탈취할 수 있습니다. 감사받은 컨트랙트에만 위임하세요. ### 핵심 요점 * EIP-7702를 사용하면 EOA가 새로운 계정 유형으로 마이그레이션하지 않고도 스마트 컨트랙트 로직을 실행할 수 있습니다. * Stable에서 EIP-7702는 기존 EOA에서 일괄 결제, 지출 한도, 범위가 지정된 세션 키를 가능하게 합니다. * 위임은 명시적으로 변경될 때까지 지속됩니다. 항상 감사받은 위임 컨트랙트를 사용하세요. ### 다음 추천 * [**구독 및 수금**](/ko/how-to/subscribe-and-collect) — SubscriptionManager를 사용하여 반복 구독 결제에 EIP-7702를 적용합니다. * [**EIP-7702 개념**](/ko/explanation/eip-7702) — 배포하기 전에 위임 모델을 이해하세요. * [**EIP-7702 레퍼런스**](/ko/reference/eip-7702-api) — `0x04` 트랜잭션 형식과 권한 부여 필드를 찾아보세요. ## Stable에서 MPP 엔드포인트 구축하기 이 가이드는 Stable에서 USDT0를 위한 커스텀 [MPP](/ko/explanation/mpp) 결제 메서드를 작성하고 MPP로 보호되는 엔드포인트를 제공하는 과정을 설명합니다. 구매자는 [ERC-3009](/ko/explanation/erc-3009) `transferWithAuthorization`에 서명하고, 서버는 `mppx`의 `verify()` 훅을 통해 이를 검증하며, 정산은 여러분이 제어하는 별도의 단계에서 이루어집니다. :::note **개념:** MPP가 무엇이며 x402와 어떤 관련이 있는지는 [Machine Payments Protocol (MPP)](/ko/explanation/mpp)를 참고하세요. x402에 해당하는 내용은 [호출당 과금 API 구축하기](/ko/how-to/build-pay-per-call)를 참고하세요. ::: :::note 이 예제는 Stable 메인넷을 사용합니다. 테스트할 때는 소액을 사용하세요. ::: ### 무엇을 만들 것인가 `402 Payment Required`와 MPP `WWW-Authenticate` 챌린지를 반환하고, `Authorization` 헤더에 담긴 서명된 자격 증명을 수락하여 이를 검증하고, USDT0에서 `transferWithAuthorization`을 정산한 후, `Payment-Receipt` 헤더와 함께 응답을 반환하는 HTTP 엔드포인트입니다. ```text step 1. Client: GET /weather (no Authorization header) Server: 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[...usdt0-stable charge for $0.001...]" step 2. Client signs an ERC-3009 authorization with their viem account step 3. Client: GET /weather + Authorization header containing the serialized credential Server: verify() validates the EIP-712 signature Server: settle() submits transferWithAuthorization on Stable (~700ms block confirmation) Server: 200 OK { weather: "sunny" } Payment-Receipt: reference="0x8f3a...", status="success" step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a... ``` ### 사전 준비 * Stable에 자금이 있는 USDT0 지갑. [Faucet 사용하기](/ko/how-to/use-faucet) 또는 [USDT0 옮기기](/ko/tutorial/send-usdt0)를 참고하세요. * `mppx`, `viem`, `zod`가 설치된 Node 20+. * Stable의 판매자 계정(EOA). 기본 정산 경로에서는 판매자가 USDT0로 가스를 지불합니다. [Gas Waiver](#alternative-settle-through-the-gas-waiver) 섹션에서는 가스가 없는 변형을 보여줍니다. ```bash npm install mppx viem zod express ``` ### 1. 공유 스키마 정의 `Method.from()`은 의도(intent)와 요청(Challenge) 및 자격 증명 페이로드에 대한 스키마를 선언합니다. 클라이언트와 서버 모두 이 정의를 임포트합니다. ```typescript // src/method.ts import { Method } from "mppx"; import { z } from "zod"; import { parseUnits } from "viem"; export const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; export const CHAIN_ID = 988; // Request: the Challenge payload the server sends to the client. const zRequest = z.pipe( z.object({ chainId: z.literal(CHAIN_ID), asset: z.literal(USDT0_STABLE), amount: z.string(), // human-readable, e.g. "0.001" decimals: z.literal(6), payTo: z.string().regex(/^0x[a-fA-F0-9]{40}$/), validAfter: z.number().int().nonnegative(), validBefore: z.number().int().positive(), nonce: z.string().regex(/^0x[a-fA-F0-9]{64}$/), }), z.transform(({ amount, decimals, ...rest }) => ({ ...rest, amount: parseUnits(amount, decimals).toString(), // atomic units })), ); // Credential payload: what the client returns after signing. const zPayload = z.object({ from: z.string().regex(/^0x[a-fA-F0-9]{40}$/), signature: z.string().regex(/^0x[a-fA-F0-9]{130}$/), // 65-byte hex }); export const usdt0Stable = Method.from({ intent: "charge", name: "usdt0-stable", schema: { request: zRequest, credential: { payload: zPayload } }, }); // EIP-712 domain + type, used by both client and server. export const EIP712_DOMAIN = { name: "USDT0", version: "1", chainId: CHAIN_ID, verifyingContract: USDT0_STABLE, } as const; export const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], } as const; ``` ```text usdt0Stable.name === "usdt0-stable" usdt0Stable.intent === "charge" ``` ### 2. 서버: 자격 증명 검증 `Method.toServer`는 `verify()`를 `mppx`에 연결합니다. 이 함수는 역직렬화된 자격 증명(챌린지 + 페이로드)을 받아서 유효하지 않은 증명에 대해 throw하거나 `Receipt`를 반환해야 합니다. ```typescript // src/server-method.ts import { Method, Receipt } from "mppx"; import { verifyTypedData } from "viem"; import { usdt0Stable, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPES, } from "./method"; export const usdt0StableServer = Method.toServer(usdt0Stable, { async verify({ credential }) { const { request } = credential.challenge; const { from, signature } = credential.payload; const valid = await verifyTypedData({ address: from as `0x${string}`, domain: EIP712_DOMAIN, types: TRANSFER_WITH_AUTHORIZATION_TYPES, primaryType: "TransferWithAuthorization", message: { from: from as `0x${string}`, to: request.payTo as `0x${string}`, value: BigInt(request.amount), validAfter: BigInt(request.validAfter), validBefore: BigInt(request.validBefore), nonce: request.nonce as `0x${string}`, }, signature: signature as `0x${string}`, }); if (!valid) throw new Error("Invalid ERC-3009 signature"); // The Receipt's reference is filled in with the tx hash after settle(). return Receipt.from({ method: usdt0Stable.name, reference: "pending", status: "success", timestamp: new Date().toISOString(), }); }, }); ``` ```text { method: "usdt0-stable", reference: "pending", status: "success", timestamp: "2026-06-01T12:34:56.000Z" } ``` :::warning `verify()`는 서명을 확인하지만 nonce의 고유성이나 권한이 이미 사용되었는지 여부는 확인하지 않습니다. 체인은 제출 시점에 두 가지를 모두 강제합니다. `transferWithAuthorization`은 이미 사용된 nonce에 대해 revert됩니다. 정산 단계는 이러한 revert를 서버가 클라이언트에 노출할 수 있는 오류로 변환합니다. ::: ### 3. 정산: `transferWithAuthorization` 제출 정산은 의도적으로 `verify()`와 분리되어 있습니다. `verify()`가 반환된 후, 여러분의 운영 모델에 맞는 경로를 통해 권한을 온체인에 제출합니다. 권장 순서대로 세 가지 옵션이 있습니다. #### 기본: 서버가 직접 제출 판매자의 EOA가 서명된 권한과 함께 `transferWithAuthorization`을 USDT0에 제출합니다. 판매자는 USDT0(Stable의 네이티브 가스 토큰)로 가스를 지불하므로, 관리해야 할 별도의 가스 토큰 잔액이 없습니다. ```typescript // src/settle.ts import { createWalletClient, http, parseSignature } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { stable } from "viem/chains"; import { USDT0_STABLE } from "./method"; const USDT0_ABI = [ { name: "transferWithAuthorization", type: "function", stateMutability: "nonpayable", inputs: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, { name: "v", type: "uint8" }, { name: "r", type: "bytes32" }, { name: "s", type: "bytes32" }, ], outputs: [], }, ] as const; const seller = privateKeyToAccount(process.env.SELLER_KEY as `0x${string}`); const wallet = createWalletClient({ account: seller, chain: stable, transport: http("https://rpc.stable.xyz"), }); export async function settleDirect(credential: { challenge: { request: any }; payload: { from: string; signature: string }; }): Promise<{ txHash: `0x${string}` }> { const { request } = credential.challenge; const { v, r, s } = parseSignature(credential.payload.signature as `0x${string}`); const txHash = await wallet.writeContract({ address: USDT0_STABLE, abi: USDT0_ABI, functionName: "transferWithAuthorization", args: [ credential.payload.from as `0x${string}`, request.payTo as `0x${string}`, BigInt(request.amount), BigInt(request.validAfter), BigInt(request.validBefore), request.nonce as `0x${string}`, Number(v), r as `0x${string}`, s as `0x${string}`, ], }); return { txHash }; } ``` ```text { txHash: "0x8f3a1b2c..." } ``` #### 대안: Gas Waiver를 통한 정산 Stable의 [Gas Waiver](/ko/how-to/integrate-gas-waiver)를 사용하여 내부 트랜잭션을 `gasPrice = 0`으로 제출합니다. 판매자는 여전히 래핑 트랜잭션에 서명하지만 가스는 지불하지 않습니다. Waiver Server API 키가 필요합니다. ```typescript // src/settle-waiver.ts import { encodeFunctionData } from "viem"; import { USDT0_STABLE } from "./method"; import { USDT0_ABI } from "./settle"; const WAIVER_SERVER = "https://waiver.stable.xyz"; // mainnet endpoint export async function settleViaWaiver( credential: { challenge: { request: any }; payload: { from: string; signature: string } }, signedInnerTxHex: `0x${string}`, ): Promise<{ txHash: `0x${string}` }> { const res = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.WAIVER_API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex] }), }); const lines = (await res.text()).trim().split("\n"); const result = JSON.parse(lines[0]); if (!result.success) throw new Error(`Settle failed: ${result.error?.message}`); return { txHash: result.txHash }; } ``` ```text { txHash: "0x8f3a1b2c..." } ``` 게시 전에 서명된 내부 트랜잭션(`gasPrice: 0`, 인코딩된 `transferWithAuthorization` 호출)을 구성하는 방법은 [Gas waiver 프로토콜](/ko/reference/gas-waiver-api)을 참고하세요. #### 대안: x402 facilitator에 위임 이미 x402 facilitator 통합([Semantic Pay](https://docs.semanticpay.io) 또는 [Heurist](https://docs.heurist.ai/x402-products/facilitator))을 운영 중이라면, 이를 정산 대상으로 재사용할 수 있습니다. `/settle`에 `paymentPayload`를 POST하면 facilitator가 온체인 호출을 제출합니다. 정확한 `paymentPayload` 형태는 x402 미들웨어 내부에 있으며 와이어 수준에서 명시되지 않습니다. 가장 간단한 경로는 facilitator의 자체 SDK를 사용하여 페이로드를 구성하거나, 위의 직접 제출 경로를 고수하는 것입니다. facilitator는 MPP를 이해할 필요가 없으며, `transferWithAuthorization` 필드만 보게 됩니다. ### 4. 클라이언트: 자격 증명 서명 `Method.toClient`는 `createCredential()`을 `mppx`에 연결합니다. 클라이언트는 Challenge를 읽고, 에이전트의 viem 계정으로 EIP-712 권한에 서명하며, 자격 증명을 직렬화합니다. ```typescript // src/client-method.ts import { Credential, Method } from "mppx"; import { hexToSignature, parseSignature } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { usdt0Stable, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPES, } from "./method"; export function createUsdt0StableClient(privateKey: `0x${string}`) { const account = privateKeyToAccount(privateKey); return Method.toClient(usdt0Stable, { async createCredential({ challenge }) { const { request } = challenge; const signature = await account.signTypedData({ domain: EIP712_DOMAIN, types: TRANSFER_WITH_AUTHORIZATION_TYPES, primaryType: "TransferWithAuthorization", message: { from: account.address, to: request.payTo as `0x${string}`, value: BigInt(request.amount), validAfter: BigInt(request.validAfter), validBefore: BigInt(request.validBefore), nonce: request.nonce as `0x${string}`, }, }); return Credential.serialize({ challenge, payload: { from: account.address, signature }, }); }, }); } ``` ```text "eyJjaGFsbGVuZ2UiOnsi..." // base64-serialized credential, ~600 bytes ``` ### 5. 서버 연결하기 `mppx`의 Express 미들웨어를 사용하여 Challenge를 발급하고, 들어오는 `Authorization` 헤더를 파싱하고, `verify()`를 실행하고, 정산 함수를 호출하고, `Payment-Receipt` 헤더를 내보냅니다. ```typescript // src/server.ts import express from "express"; import { Mppx } from "mppx/express"; import { randomBytes } from "node:crypto"; import { usdt0StableServer } from "./server-method"; import { settleDirect } from "./settle"; const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`; const PORT = Number(process.env.PORT ?? 4022); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY!, methods: [usdt0StableServer], onVerified: async ({ credential, receipt }) => { const { txHash } = await settleDirect(credential); return { ...receipt, reference: txHash }; }, }); const app = express(); app.get( "/weather", mppx.charge({ amount: "0.001", method: "usdt0-stable", request: { chainId: 988, asset: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", decimals: 6, payTo: PAY_TO, validAfter: 0, validBefore: Math.floor(Date.now() / 1000) + 300, nonce: `0x${randomBytes(32).toString("hex")}`, }, })((_req, res) => { res.json({ weather: "sunny", temperature: 70 }); }), ); app.listen(PORT, () => { console.log(`MPP server listening on http://localhost:${PORT}`); }); ``` ```text MPP server listening on http://localhost:4022 ``` ### 6. 전체 흐름을 끝까지 실행하기 서버를 시작하고, Challenge를 확인하고, 클라이언트를 실행한 후, 정산을 확인합니다. :::warning 다음 단계는 Stable 메인넷에서 실제 USDT0 결제를 정산합니다. 전용 지갑과 소액을 사용하세요. ::: #### Challenge 확인하기 ```bash curl -i http://localhost:4022/weather ``` ```text HTTP/1.1 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[{\"method\":\"usdt0-stable\",\"request\":{...}}]" Content-Type: application/json {"error":"Payment required"} ``` #### 유료 요청 보내기 ```typescript // src/client.ts import { Mppx } from "mppx/client"; import { createUsdt0StableClient } from "./client-method"; const client = Mppx.create({ methods: [createUsdt0StableClient(process.env.BUYER_KEY as `0x${string}`)], }); const res = await fetch("http://localhost:4022/weather", { // mppx wraps fetch with the 402 retry loop: ...client.fetchOptions(), }); console.log(res.status, await res.json()); console.log("Payment-Receipt:", res.headers.get("Payment-Receipt")); ``` ```bash npx tsx src/client.ts ``` ```text 200 { weather: "sunny", temperature: 70 } Payment-Receipt: reference="0x8f3a1b2c...", status="success", timestamp="2026-06-01T12:34:56.000Z" ``` #### Stablescan에서 확인하기 `https://stablescan.xyz/tx/0x8f3a1b2c...`를 열고 `transferWithAuthorization`이 여러분의 `PAY_TO` 주소로 정산되었는지 확인하세요. ### 방금 한 일 * 구매자 측에서 관리할 가스 토큰 잔액 없이, 달러로 표시된 USDT0로 결제했습니다. * 클라이언트-서버 홉에서 MPP의 `WWW-Authenticate` / `Authorization` / `Payment-Receipt` 와이어 형식을 사용했습니다. * 동일한 HTTP 요청 수명 주기 내에서 Stable의 `transferWithAuthorization`으로 정산했습니다(\~700ms 블록 타임). ### 다음 권장 사항 * [**MPP 개념**](/ko/explanation/mpp) — MPP가 x402와 어떤 관련이 있는지, 그리고 다른 의도들이 어떻게 생겼는지 읽어보세요. * [**MPP 세션**](/ko/explanation/mpp-sessions) — 요청당 정산이 너무 비쌀 때 오프체인 바우처로 마이크로페이먼트를 스트리밍하세요. * [**Facilitators**](/ko/reference/agentic-facilitators) — 직접 제출하는 대신 Semantic Pay나 Heurist를 정산 대상으로 사용하세요. ## P2P 결제 배우기 이 가이드는 Stable에서 P2P 결제 애플리케이션을 구축하는 과정을 안내합니다. 이 앱은 결제의 전체 수명 주기를 처리합니다. 발신자는 USDT0를 직접 전송하고, 수신자는 들어오는 결제를 실시간으로 감지하며, 양쪽 모두 자신의 거래 내역을 조회할 수 있습니다. 모바일 앱이든 웹 체크아웃이든 백엔드 서비스든, 모든 지갑이나 결제 인터페이스와 동일한 아키텍처입니다. 미들웨어도 중개자도 없습니다. 개념적 개요는 [P2P 결제](/ko/reference/p2p-payments)를 참고하세요. ABI 작업을 건너뛰고 몇 줄로 작동하는 `transfer`에 도달하려면 [Stable SDK](/ko/explanation/sdk-overview)를 사용하세요. ### 구축할 내용 최소한의 결제 앱을 구성하는 다섯 개의 스크립트: * `wallet.ts` — 지갑 생성 또는 복원. * `getBalance.ts` — 현재 USDT0 잔액 조회. * `send.ts` — 다른 주소로 USDT0 전송. * `receive.ts` — 들어오는 결제를 실시간으로 감시. * `history.ts` — 특정 주소의 과거 Transfer 이벤트 조회. #### 데모 ```text step 1. Alice creates wallet → address: 0xAlice... step 2. Alice's balance: 0.01 USDT0 step 3. Alice sends 0.001 USDT0 to Bob tx: 0x8f3a...2d41 gas fee: 0.000021 USDT0 Alice balance: 0.008979 USDT0 step 4. Bob receives payment (real-time event) from: 0xAlice... amount: 0.001 USDT0 tx: 0x8f3a...2d41 ``` ### 사전 요구 사항 * Node.js 20 이상. * 테스트넷 USDT0가 있는 개인 키 (지갑에 자금을 충전하려면 [빠른 시작](/ko/tutorial/quick-start) 참고). ### 프로젝트 설정 ```bash mkdir stable-p2p && cd stable-p2p npm init -y && npm install ethers dotenv ``` ```text added 2 packages, audited 3 packages in 1s ``` 모든 스크립트가 공유하는 `config.ts`를 생성합니다: ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_WS = "wss://rpc.testnet.stable.xyz"; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_RPC); ``` ### 1. 지갑 생성 또는 복원 지갑은 시드 문구에서 파생된 키 쌍입니다. 새 사용자를 위해 하나를 생성하고 백업할 수 있도록 문구를 반환합니다. 돌아온 사용자는 동일한 문구로 지갑을 복원합니다. ```typescript // wallet.ts import { ethers } from "ethers"; import { provider } from "./config"; /** Create a new wallet for a new user. */ export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // display to user for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address }; } if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase); } ``` ```bash npx tsx wallet.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` ### 2. 잔액 확인 USDT0는 Stable의 네이티브 자산이므로, 잔액 조회는 Ethereum의 ETH와 정확히 동일하게 작동합니다. 네이티브 잔액은 소수점 18자리이며, 표시할 때는 `formatEther`를 사용합니다. ```typescript // getBalance.ts import { ethers } from "ethers"; import { provider } from "./config"; export async function getBalance(address: string) { const balance = await provider.getBalance(address); return ethers.formatEther(balance); // 18 decimals } if (import.meta.url === `file://${process.argv[1]}`) { const address = process.argv[2]; const balance = await getBalance(address); console.log("Balance:", balance, "USDT0"); } ``` ```bash npx tsx getBalance.ts 0xAlice...1234 ``` ```text Balance: 0.01 USDT0 ``` ### 3. 결제 전송 발신자는 전송을 직접 서명하고 제출합니다. Stable에서 USDT0는 네이티브 자산이므로, 단순한 value 전송이 가장 저렴한 경로입니다(21,000 가스). 이는 모든 결제 앱에서 "보내기"와 동일한 코드 경로입니다. ```typescript // send.ts import { ethers } from "ethers"; import { provider } from "./config"; export async function sendPayment( senderKey: string, recipient: string, amount: string // e.g. "0.001" for 0.001 USDT0 ) { const wallet = new ethers.Wallet(senderKey, provider); const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: ethers.parseEther(amount), maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); console.log("Payment sent:", tx.hash); const receipt = await tx.wait(1); if (receipt!.status === 1) console.log("Payment settled"); return tx.hash; } if (import.meta.url === `file://${process.argv[1]}`) { const [, , recipient, amount] = process.argv; await sendPayment(process.env.PRIVATE_KEY!, recipient, amount); } ``` ```bash npx tsx send.ts 0xBob...5678 0.001 ``` ```text Payment sent: 0x8f3a...2d41 Payment settled ``` ### 4. 결제 실시간 수신 수신자는 들어오는 `Transfer` 이벤트를 수신 대기합니다. 이는 전통적인 결제 앱의 푸시 알림과 동일합니다. Stable에서는 단일 슬롯 완결성(single-slot finality) 덕분에 수신자가 거의 즉시 결제를 확인할 수 있습니다. ```typescript // receive.ts import { ethers } from "ethers"; import { STABLE_WS, USDT0_ADDRESS } from "./config"; const wsProvider = new ethers.WebSocketProvider(STABLE_WS); const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], wsProvider ); export function watchIncomingPayments(address: string) { const filter = usdt0.filters.Transfer(null, address); usdt0.on(filter, (from: string, to: string, value: bigint, event: any) => { console.log("Payment received:"); console.log(" from: ", from); console.log(" amount:", ethers.formatUnits(value, 6), "USDT0"); console.log(" tx: ", event.log.transactionHash); }); console.log("Watching for incoming payments to", address); } if (import.meta.url === `file://${process.argv[1]}`) { watchIncomingPayments(process.argv[2]); } ``` ```bash npx tsx receive.ts 0xBob...5678 ``` ```text Watching for incoming payments to 0xBob...5678 Payment received: from: 0xAlice...1234 amount: 0.001 USDT0 tx: 0x8f3a...2d41 ``` :::note USDT0는 Stable에서 네이티브 자산이면서 동시에 ERC-20 토큰이기 때문에, 네이티브 전송(value 전송)도 USDT0 ERC-20 컨트랙트에서 `Transfer` 이벤트를 발생시킵니다. 단일 이벤트 리스너로 두 가지 전송 방식을 모두 처리할 수 있습니다. ::: ### 5. 거래 내역 조회 과거 `Transfer` 이벤트를 조회하여 은행 명세서나 모든 결제 앱의 거래 목록과 같은 거래 내역 화면을 구성합니다. ```typescript // history.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], provider ); export async function getTransactionHistory(address: string, fromBlock?: number) { if (fromBlock === undefined) { const latest = await provider.getBlockNumber(); fromBlock = Math.max(0, latest - 10_000); } const [sentEvents, receivedEvents] = await Promise.all([ usdt0.queryFilter(usdt0.filters.Transfer(address, null), fromBlock), usdt0.queryFilter(usdt0.filters.Transfer(null, address), fromBlock), ]); return [ ...sentEvents.map((e: any) => ({ type: "sent" as const, counterparty: e.args[1], amount: ethers.formatUnits(e.args[2], 6), txHash: e.transactionHash, block: e.blockNumber, })), ...receivedEvents.map((e: any) => ({ type: "received" as const, counterparty: e.args[0], amount: ethers.formatUnits(e.args[2], 6), txHash: e.transactionHash, block: e.blockNumber, })), ].sort((a, b) => b.block - a.block); } if (import.meta.url === `file://${process.argv[1]}`) { const history = await getTransactionHistory(process.argv[2]); for (const tx of history) { console.log(`${tx.type} ${tx.amount} USDT0 ${tx.counterparty} ${tx.txHash}`); } } ``` ```bash npx tsx history.ts 0xAlice...1234 ``` ```text sent 0.001 USDT0 0xBob...5678 0x8f3a...2d41 received 0.01 USDT0 0xFaucet... 0x22b1...3f09 ``` :::warning 넓은 블록 범위(수백만 개의 블록)를 스캔하면 타임아웃이 발생하고 RPC 속도 제한을 초과할 수 있습니다. 프로덕션 환경에서는 페이지네이션 방식의 내역 조회를 위해 [Stablescan Etherscan 호환 API](https://stablescan.xyz)를 사용하세요 — 모든 거래가 이미 인덱싱되어 있습니다. ::: ### 다음 추천 * [**구독 및 수금**](/ko/how-to/subscribe-and-collect) — EIP-7702 위임을 사용한 풀 기반 정기 구독. * [**인보이스로 결제하기**](/ko/how-to/pay-with-invoice) — ERC-3009와 결정론적 nonce로 인보이스를 정산합니다. * [**첫 USDT0 보내기**](/ko/tutorial/send-usdt0) — 기본적인 네이티브 vs. ERC-20 전송 흐름을 참고하세요. ## 유료 호출 API 구축하기 이 가이드는 x402로 API 엔드포인트를 수익화하는 과정을 안내합니다. 서버는 결제 핸들러를 추가하고, 클라이언트는 요청마다 비용을 지불하며, 정산은 HTTP 라이프사이클 내에서 이루어집니다. :::note **개념:** x402 프로토콜과 Stable에 적합한 이유는 [x402](/ko/explanation/x402)를 참고하세요. 상위 수준의 사용 사례 모델은 [유료 호출 API](/ko/reference/pay-per-call)를 참고하세요. ::: :::note Semantic 퍼실리테이터는 현재 메인넷에서만 작동합니다. 이 가이드의 예제는 Stable 메인넷을 사용합니다. 테스트 시에는 소액만 사용하세요. ::: ### 무엇을 구축하는가 서버가 `402 Payment Required`로 응답하고, 클라이언트가 요청마다 결제하며, 퍼실리테이터가 HTTP 라이프사이클 내에서 USDT0를 온체인으로 정산하는 유료 HTTP API입니다. #### 데모 ```text step 1. Client: GET /weather (no payment) Server: 402 Payment Required PAYMENT-REQUIRED: { amount: "1000", asset: USDT0, network: eip155:988 } step 2. Client signs ERC-3009 authorization step 3. Client: GET /weather + PAYMENT-SIGNATURE header Server: forwards to facilitator → transferWithAuthorization settles on-chain (~700ms block confirmation) Server: 200 OK { weather: "sunny", temperature: 70 } PAYMENT-SETTLE-RESPONSE: { txHash: "0x8f3a...", paid: "0.001 USDT0" } step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a... ``` ### 개요 **판매자 (서버):** ```typescript // --- Server --- app.use(paymentMiddleware({ "GET /weather": { price: { amount: "1000", asset: USDT0 }, payTo: sellerAddress, }, "POST /inference": { price: { amount: "50000", asset: USDT0 }, payTo: sellerAddress, }, }, resourceServer)); // Routes not listed in the config are not gated. ``` **구매자 (클라이언트):** ```typescript // --- Client --- account = new WalletAccountEvm(seedPhrase, { provider: RPC }); client = new x402Client(); fetchWithPayment = wrapFetchWithPayment(fetch, client); weatherResponse = fetchWithPayment("https://api.example.com/weather"); inferenceResponse = fetchWithPayment("https://api.example.com/inference", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: "Hello" }), }); // For each paid request: // 1. Initial request returns 402 with PAYMENT-REQUIRED header // 2. Client signs ERC-3009 authorization with wallet // 3. Client retries with PAYMENT-SIGNATURE header // 4. Facilitator settles on-chain, server returns the response ``` ### 판매자: 유료 엔드포인트 설정하기 판매자는 어떤 라우트가 결제를 요구하는지 정의하기 위해 x402 미들웨어를 추가합니다. 결제 없이 요청이 도착하면, 미들웨어는 `402 Payment Required`와 결제 조건으로 응답합니다. 유효한 결제 헤더가 있으면, 미들웨어는 이를 퍼실리테이터로 전달하고 퍼실리테이터가 서명을 검증한 뒤 결제를 온체인으로 정산합니다. 판매자는 가격과 수령 주소만 구성하면 되고, 검증과 정산은 퍼실리테이터가 처리합니다. ```bash npm install express @x402/express @x402/evm @x402/core ``` #### 가격 책정 각 라우트는 결제 금액을 USDT0 기본 단위(소수점 6자리), 네트워크, 자금을 수령할 주소로 지정합니다. 예를 들어 `"1000"`은 `$0.001`이고 `"50000"`은 `$0.05`입니다. ```typescript price: { amount: "1000", // base units (6 decimals) asset: USDT0_STABLE, // USDT0 contract address extra: { name: "USDT0", version: "1", decimals: 6 }, // EIP-712 domain info } ``` `extra` 필드(`name`, `version`, `decimals`)는 구매자 클라이언트가 EIP-712 서명을 구성하는 데 사용되며, 온체인 USDT0 컨트랙트와 일치해야 합니다. #### 라우트 구성 라우트는 `METHOD /path` 형식으로 매핑됩니다. 각 라우트는 허용되는 결제 스킴, 네트워크, 가격, 자금을 수령할 주소(`payTo`)를 지정합니다. `description`과 `mimeType` 필드는 구매자와 AI 에이전트가 엔드포인트가 제공하는 것을 발견하는 데 도움이 됩니다. 구성에 나열되지 않은 라우트는 게이트되지 않으며 일반 Express 라우트처럼 동작합니다. ```typescript // server.ts import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`; const FACILITATOR_URL = "https://x402.semanticpay.io/"; const STABLE_NETWORK = "eip155:988"; // Stable Mainnet CAIP-2 ID const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL }); const resourceServer = new x402ResourceServer(facilitatorClient) .register(STABLE_NETWORK, new ExactEvmScheme()); const app = express(); app.use( paymentMiddleware( { // Example 1: Configure a paid GET route "GET /weather": { accepts: [ { scheme: "exact", network: STABLE_NETWORK, price: { amount: "1000", // $0.001 asset: USDT0_STABLE, extra: { name: "USDT0", version: "1", decimals: 6 }, }, payTo: PAY_TO, }, ], description: "Weather data", mimeType: "application/json", }, // Example 2: Configure a paid POST route "POST /inference": { accepts: [ { scheme: "exact", network: STABLE_NETWORK, price: { amount: "50000", // $0.05 asset: USDT0_STABLE, extra: { name: "USDT0", version: "1", decimals: 6 }, }, payTo: PAY_TO, }, ], description: "AI inference endpoint", mimeType: "application/json", }, }, resourceServer, ), ); app.get("/weather", (req, res) => { res.json({ weather: "sunny", temperature: 70 }); }); app.post("/inference", (req, res) => { const { prompt } = req.body; res.json({ result: `Inference result for: ${prompt}` }); }); // Not listed in the config, so no payment required. app.get("/health", (req, res) => { res.json({ status: "ok", payTo: PAY_TO }); }); const PORT = process.env.PORT || 4021; app.listen(PORT, () => { console.log(`Server listening at http://localhost:${PORT}`); console.log(`GET /health - free`); console.log(`GET /weather - $0.001 per request`); console.log(`POST /inference - $0.05 per request`); }); ``` :::note x402는 Hono(`@x402/hono`)와 Next.js(`@x402/next`)용 미들웨어도 제공합니다. 패턴은 동일합니다: 퍼실리테이터 클라이언트를 생성하고, EVM 스킴을 등록하고, 미들웨어를 적용합니다. ::: ### 구매자: 유료 요청하기 구매자는 수동 결제 흐름을 거치지 않고도 유료 엔드포인트에 접근합니다. 구매자는 가스비를 지불하지 않습니다. 퍼실리테이터가 온체인으로 정산하며, 구매자는 결제 요구사항에 지정된 정확한 금액만 지불합니다. ```bash npm install @x402/fetch @x402/evm @tetherto/wdk-wallet-evm ``` #### 지갑 생성 및 잔액 확인 ```typescript // client.ts import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, { provider: "https://rpc.stable.xyz", }).getAccount(0); console.log("Buyer address:", account.address); // USDT0 uses 6 decimals. A balance of 1000000 equals 1.00 USDT0. const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const balance = await account.getTokenBalance(USDT0_STABLE); console.log("USDT0 balance:", Number(balance) / 1e6, "USDT0"); ``` #### x402에 연결하고 유료 요청하기 `WalletAccountEvm`은 x402가 기대하는 서명자 인터페이스를 충족하므로, x402 클라이언트의 서명자로 직접 등록할 수 있습니다. 등록되면 x402 지원 클라이언트를 통해 전송된 요청은 402 결제 흐름을 자동으로 처리합니다. ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; const client = new x402Client(); registerExactEvmScheme(client, { signer: account }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("http://localhost:4021/weather"); const data = await response.json(); console.log("Response:", data); ``` 내부적으로 `fetchWithPayment`는 402 응답을 가로채고, 결제 요구사항(금액, 토큰, 네트워크, 수령인)을 파싱하며, WDK 지갑으로 ERC-3009 `transferWithAuthorization`에 서명하고, `PAYMENT-SIGNATURE` 헤더와 함께 요청을 재시도합니다. :::note Axios를 선호한다면, 동일한 자동 결제 처리를 위해 `@x402/axios`와 `wrapAxiosWithPayment`를 사용하세요. ::: ### 결제 흐름 테스트하기 서버를 시작하고 유료 및 무료 라우트를 모두 검증합니다. :::warning 이 테스트 흐름은 Stable 메인넷에서 실행됩니다. 성공한 각 유료 요청은 호스팅된 퍼실리테이터를 통해 실제 USDT0 결제를 정산합니다. 전용 지갑과 소액만 사용하세요. ::: #### 1. 402 응답 확인하기 ```bash curl -i http://localhost:4021/weather ``` 응답은 가격, 자산, 네트워크를 포함하는 `PAYMENT-REQUIRED` 헤더와 함께 `402 Payment Required`여야 합니다. #### 2. 클라이언트 실행하기 ```bash npx tsx client.ts ``` 클라이언트는 전체 주기를 처리합니다: 402를 받고, 권한을 서명하고, 결제와 함께 재시도하고, 응답을 출력합니다. #### 3. 영수증 읽기 성공한 유료 요청 후, 구매자는 서버 응답에서 `PAYMENT-SETTLE-RESPONSE` 헤더를 읽고 정산 영수증을 파싱할 수 있습니다. ```typescript // (continued) client.ts import { x402HTTPClient } from "@x402/fetch"; const httpClient = new x402HTTPClient(client); const receipt = httpClient.getPaymentSettleResponse( (name) => response.headers.get(name), ); console.log("Payment receipt:", JSON.stringify(receipt, null, 2)); ``` ### 라이브 퍼실리테이터 없이 테스트하기 Semantic 퍼실리테이터는 메인넷 전용이므로, 현재 서버를 테스트넷 퍼실리테이터로 연결할 수 없습니다. 실제 결제를 정산하지 않고 서버 로직, 라우트 핸들러, 미들웨어 동작을 반복 개발하려면, 퍼실리테이터 클라이언트를 스텁으로 대체하세요. ```typescript // server.test.ts import { x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; // Stub facilitator: accepts any signature, returns a fake settlement. const stubFacilitatorClient = { verify: async () => ({ isValid: true, payer: "0xMockPayer" }), settle: async () => ({ success: true, txHash: "0xMOCK000000000000000000000000000000000000000000000000000000000001", networkId: "eip155:988", }), }; export const testResourceServer = new x402ResourceServer(stubFacilitatorClient as any) .register("eip155:988", new ExactEvmScheme()); ``` 스텁에 대해 유닛 테스트를 실행하여 다음을 검증합니다: * 402 응답에 올바른 `PAYMENT-REQUIRED` 페이로드가 포함됩니다. * 유효한 `PAYMENT-SIGNATURE` 헤더가 있는 요청이 핸들러에 도달합니다. * 헤더가 누락되었거나 형식이 잘못된 요청은 핸들러 실행 전에 거부됩니다. 실제 정산을 실행할 준비가 되면, `HTTPFacilitatorClient`로 다시 전환하고 소액으로 메인넷에서 실행하세요. :::warning 스텁 정산은 미들웨어 동작만 검증합니다. 실제 네트워크 지연이나 동시 결제 상황에서 라우트 핸들러가 멱등성을 갖는지는 증명하지 않습니다. 출시 전에 항상 소액으로 라이브 메인넷 테스트를 마무리하세요. ::: ### 고급: 라이프사이클 훅 x402는 흐름의 주요 지점에서 결제 처리를 가로채고 커스터마이징할 수 있는 훅을 제공합니다. 예를 들어, 서버는 검증 전에 로직을 실행하여(예: API 키 또는 구독자 상태 확인) 승인된 요청에 대해 결제를 우회할 수 있고, 클라이언트는 서명 전에 지출 한도를 적용할 수 있습니다. 전체 훅 레퍼런스와 예제는 [x402 라이프사이클 훅](https://x402.semanticpay.io/docs/hooks)을 참고하세요. ### 다음 추천 * [**x402 개념**](/ko/explanation/x402) — 프로토콜과 그것이 적용되는 위치를 이해하세요. * [**ERC-3009**](/ko/explanation/erc-3009) — x402가 사용하는 정산 표준을 검토하세요. * [**MCP 서버로 결제하기**](/ko/how-to/pay-with-mcp) — AI 클라이언트가 프롬프트를 통해 호출할 수 있도록 이 API를 MCP 도구로 래핑하세요. ## 지갑 생성하기 Stable 지갑은 이더리움 표준 키 쌍입니다. EVM 계정을 생성하는 모든 지갑 라이브러리는 수정 없이 Stable에서 작동합니다. 이 가이드에서는 두 가지 경로를 소개합니다: 대부분의 애플리케이션을 위한 ethers.js, 그리고 에이전트와 결제를 위한 턴키 방식의 자가 수탁(self-custody) 계층을 원하는 통합을 위한 Tether의 [WDK (Wallet Development Kit)](https://github.com/tetherto/wdk)입니다. :::note 등록도, Stable 전용 계정 설정도 필요 없습니다. 지갑은 [테스트넷 포셋](/ko/how-to/use-faucet)이나 메인넷 전송으로부터 즉시 USDT0를 받을 수 있습니다. ::: ### 사전 준비 사항 * Node.js 20 이상. ### 옵션 1: ethers.js 라이브러리를 설치하고 키 쌍을 생성합니다. ```bash npm install ethers ``` ```typescript // wallet.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); /** Create a new wallet for a new user. */ export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // show to the user once for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address }; } if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase); } ``` ```bash npx tsx wallet.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` :::warning 프로덕션 환경에서는 시드 문구를 절대 평문으로 로그에 남기거나 저장하지 마세요. 저장 시 암호화하거나 시크릿 매니저를 사용하세요. `ethers.Wallet.createRandom`은 호출당 한 번만 문구를 반환합니다 — 이를 잃어버리면 자금을 복구할 수 없습니다. ::: ### 옵션 2: Tether WDK WDK는 키 파생, 서명, 트랜잭션 제출을 하나의 인터페이스로 감쌉니다. 일반적인 계정 흐름을 재구현하지 않고 자가 수탁을 원할 때 적합한 선택이며, 에이전트 결제를 위한 [x402](/ko/how-to/build-pay-per-call)와 직접 통합됩니다. ```bash npm install @tetherto/wdk @tetherto/wdk-wallet-evm ``` ```typescript // wallet-wdk.ts import WDK from "@tetherto/wdk"; import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; function initWdk(seedPhrase: string) { return new WDK(seedPhrase) .registerWallet("stable", WalletManagerEvm, { provider: "https://rpc.testnet.stable.xyz", }); } /** Create a new wallet for a new user. */ export async function createWallet() { const seedPhrase = WDK.getRandomSeedPhrase(); const wdk = initWdk(seedPhrase); const account = await wdk.getAccount("stable", 0); return { account, address: await account.getAddress(), seedPhrase, // show to the user once for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export async function restoreWallet(seedPhrase: string) { const wdk = initWdk(seedPhrase); const account = await wdk.getAccount("stable", 0); return { account, address: await account.getAddress() }; } ``` ```bash npx tsx wallet-wdk.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` ### 지갑에 자금 충전하기 지갑이 트랜잭션을 처리하려면 가스용 USDT0가 필요합니다. 테스트넷에서는 포셋에 요청하세요: ```bash open https://faucet.stable.xyz ``` 주소를 붙여넣고 버튼을 선택해 1 테스트넷 USDT0(수천 건의 네이티브 전송에 충분한 양)를 받으세요. 메인넷의 경우, 지원되는 거래소나 브리지에서 USDT0를 전송하세요. [Stable로 브리징하기](/ko/explanation/usdt0-bridging)를 참조하세요. ### 잔액 확인하기 네이티브 USDT0는 18자리 소수를 사용합니다. 네이티브 잔액이 가스가 지불되는 잔액입니다. ```typescript // balance.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const balance = await provider.getBalance("0xYourAddress"); console.log("Balance:", ethers.formatEther(balance), "USDT0"); ``` ```bash npx tsx balance.ts ``` ```text Balance: 1.0 USDT0 ``` ### 다음 권장 단계 * [**EIP-7702로 위임하기**](/ko/how-to/account-abstraction) — 이 지갑에 일괄 결제, 지출 한도, 세션 키를 추가하세요. * [**첫 USDT0 보내기**](/ko/tutorial/send-usdt0) — 동일한 잔액에서의 네이티브 및 ERC-20 전송. * [**테스트넷 지갑에 자금 충전하기**](/ko/how-to/use-faucet) — 더 큰 테스트 잔액을 위한 포셋 및 Sepolia 브리지 옵션. Stable은 MCP 서버, 에이전트 스킬, 일반 텍스트 문서 파일을 제공하여 AI 에디터와 코딩 에이전트가 Stable과 직접 작업할 수 있도록 합니다. 이 페이지에서는 각 구성 요소를 워크플로에 연결하는 방법, MCP를 지원하지 않는 AI 도구를 위한 복사-붙여넣기 가능한 컨텍스트 블록, 일반적인 작업을 위한 시작 프롬프트를 다룹니다. ### MCP 서버 Stable은 두 개의 MCP 서버를 운영합니다. **Docs MCP**는 이 문서 사이트에서 개념, 가이드, 코드 스니펫, 컨트랙트 레퍼런스를 검색합니다. **Runtime MCP**는 잔액 조회, 트랜잭션 시뮬레이션, 실행을 위해 Stable 체인과 상호작용합니다. 두 서버 모두 MCP 호환 클라이언트에 추가할 수 있습니다. #### Cursor MCP 설정 파일을 열고 다음을 추가하세요: ```json { "mcpServers": { "stable-docs": { "url": "https://docs.stable.xyz/mcp" }, "stable-runtime": { "url": "https://runtime.stable.xyz/mcp" } } } ``` Cursor를 재시작합니다. "Stable에서 USDT0를 어떻게 보내나요?"라고 물어 확인하세요. #### Claude Code ```bash claude mcp add stable-docs https://docs.stable.xyz/mcp claude mcp add stable-runtime https://runtime.stable.xyz/mcp ``` "Gas Waiver 통합 단계에 대해 Stable 문서를 검색해줘."라고 물어 확인하세요. ### 에이전트 스킬 에이전트 스킬은 Docs MCP와 Runtime MCP를 결합한 사전 정의된 워크플로입니다. "세 개의 주소로 100 USDT0를 보내줘"와 같은 작업을 AI에 요청하면, 스킬이 전체 시퀀스를 처리합니다: 관련 문서 조회, 주소 및 파라미터 확인, 잔액 점검, 트랜잭션 시뮬레이션, 승인 후 실행. 스킬은 Claude Code 플러그인으로 제공됩니다. #### 설치 ```bash claude plugin add stable-xyz/agent-skills ``` 또는 Claude Code 마켓플레이스에서 설치하세요. 전체 스킬 정의와 소스는 [agent-skills 저장소](https://github.com/stable-xyz/agent-skills)를 참고하세요. ### 일반 텍스트 문서 MCP를 지원하지 않는 AI 도구를 위해, Stable 문서는 정적 텍스트 파일로 제공됩니다. | **파일** | **URL** | **내용** | | :-------------- | :----------------------------------------------------------------------------- | :------------------ | | `llms.txt` | [https://docs.stable.xyz/llms.txt](https://docs.stable.xyz/llms.txt) | 제목과 설명이 포함된 페이지 인덱스 | | `llms-full.txt` | [https://docs.stable.xyz/llms-full.txt](https://docs.stable.xyz/llms-full.txt) | 단일 파일로 된 전체 문서 | 이 파일들은 정적 스냅샷입니다. 가장 최신 내용을 위해서는 Docs MCP를 사용하세요. #### Cursor 1. **Settings > Features > Docs**로 이동합니다. 2. **Add**를 선택하고 `https://docs.stable.xyz/llms-full.txt`를 입력합니다. 3. 채팅에서 `@Stable`로 참조합니다. #### 기타 도구 `llms-full.txt`를 다운로드하여 프로젝트 컨텍스트나 시스템 프롬프트에 포함하세요. ### Stable 컨텍스트 블록 이것을 모든 AI 채팅이나 시스템 프롬프트 상단에 붙여넣으세요. 모델이 첫 시도에 올바른 Stable 코드를 생성하는 데 필요한 모든 것을 제공합니다. ```markdown # Stable chain context Stable is a Layer 1 where USDT0 is the native gas token. Fully EVM-compatible. All standard EVM tools (Hardhat, Foundry, ethers.js, viem) work unchanged once you adjust three gas fields (see Behavioral differences below). ## Network | Field | Mainnet | Testnet | | :-------------- | :--------------------------------------- | :----------------------------------------- | | Chain ID | 988 | 2201 | | RPC | https://rpc.stable.xyz | https://rpc.testnet.stable.xyz | | Explorer | https://stablescan.xyz | https://testnet.stablescan.xyz | | Currency symbol | USDT0 | USDT0 | ## USDT0 contract addresses - Mainnet: 0x779ded0c9e1022225f8e0630b35a9b54be713736 - Testnet: 0x78cf24370174180738c5b8e352b6d14c83a6c9a9 ## Behavioral differences from Ethereum 1. **Gas token is USDT0, not ETH.** The `value` field in native transfers carries USDT0. Fees are denominated in USDT0. 2. **`maxPriorityFeePerGas` is always 0.** No tip-based ordering. Set it explicitly to `0n` or validators will reject or ignore tip components. 3. **USDT0 has a dual role**: native asset (18 decimals) AND ERC-20 (6 decimals) on the same balance. `address(x).balance` reports 18-decimal wei; `USDT0.balanceOf(x)` reports 6-decimal units. Values may differ by up to 0.000001 USDT0 due to fractional reconciliation. Never mirror native balance in an internal variable; always query at payout time. 4. **Transfer events are emitted for native transfers too.** A single Transfer event listener on the USDT0 ERC-20 contract covers both transfer paths. 5. **Single-slot finality (~700ms).** Once a block is committed, it cannot be reorged. No need to wait multiple confirmations. 6. **Gas Waiver** lets applications cover gas: user signs with `gasPrice = 0`, a governance-registered waiver wraps and submits. Contracts must be on the waiver's AllowedTarget policy. 7. **EIP-7702** is supported for delegating an EOA to a contract (type-4 tx). 8. **Precompile addresses**: Bank `0x...1003`, Distribution `0x...0801`, Staking `0x...0800`, StableSystem `0x...9999`. ## Common mistakes to avoid - Copying Ethereum priority-fee constants (2 gwei tips, etc.) — has no effect on Stable and can be rejected by wallets. - Using `ethers.parseUnits(x, 18)` for ERC-20 USDT0 amounts. ERC-20 uses 6 decimals; native transfers use 18. - Mirroring native balance in a `uint256 deposited` variable — USDT0 allowance-based operations (transferFrom, permit) can reduce a contract's native balance without invoking its code. - Sending native or ERC-20 USDT0 to `address(0)` — both revert on Stable. - Assuming `EXTCODEHASH == 0` means an address is unused. On Stable, permit-based approvals can change state without incrementing nonce. - Writing `value: ethers.parseEther(amount, "ether")` and expecting ETH semantics. That transfer sends USDT0. ``` ### 시작 프롬프트 위의 컨텍스트 블록을 로드한 후 다음 중 아무거나 AI 에디터에 복사하세요. #### 컨트랙트 배포 ```text Use Foundry to scaffold a project called `stable-escrow`. Write a minimal Escrow contract in Solidity ^0.8.24 with deposit() and withdraw(amount) functions that transfer USDT0 natively. Use address(this).balance for solvency checks (never mirror the balance in a uint256). Reject address(0) recipients. Then produce a deployment command using `forge create` pointed at Stable testnet (RPC https://rpc.testnet.stable.xyz, chain ID 2201). ``` #### USDT0 보내기 ```text Write a TypeScript script using ethers v6 that sends 0.001 USDT0 natively from the wallet loaded from PRIVATE_KEY. Use base-fee-only EIP-1559 gas (maxPriorityFeePerGas = 0n, maxFeePerGas = 2 * baseFeePerGas). Target Stable testnet. Log the tx hash and a Stablescan explorer URL. ``` #### EIP-7702 위임 설정 ```text Write a TypeScript script using ethers v6 that: 1. Signs an EIP-7702 authorization delegating my EOA to Multicall3 at 0xcA11bde05977b3631167028862bE2a173976CA11 on Stable testnet (chain ID 2201). 2. Sends a type-4 transaction with authorizationList: [signedAuth], to: wallet.address (self-call), and data that invokes aggregate3() to batch three USDT0 transfers (100, 200, 150 USDT0 with 6 decimals). 3. Use maxPriorityFeePerGas: 0n. ``` #### 구독 컨트랙트 빌드 ```text Write a SubscriptionManager Solidity contract for EIP-7702 delegation on Stable. It runs on a subscriber's EOA. Expose: - subscribe(bytes32 subId, address provider, uint256 amount, uint256 interval) callable only when msg.sender == address(this) (subscriber on their own EOA). - collect(bytes32 subId) callable only by the registered provider, only when block.timestamp >= nextChargeAt; advances nextChargeAt by interval and transfers USDT0 to the provider. Use IERC20 USDT0 at the testnet address 0x78cf24370174180738c5b8e352b6d14c83a6c9a9. - cancelSubscription(bytes32 subId) callable only by the subscriber. Emit events for SubscriptionCreated, SubscriptionCollected, SubscriptionCancelled. ``` #### x402 호출당 결제 API 빌드 ```text Write an Express server in TypeScript that exposes GET /weather priced at $0.001 USDT0 (amount: "1000", 6 decimals) using @x402/express, @x402/evm/exact/server, and HTTPFacilitatorClient pointed at https://x402.semanticpay.io/. Use Stable mainnet (CAIP-2 eip155:988, USDT0 at 0x779Ded0c9e1022225f8E0630b35a9b54bE713736). The handler should return { weather: "sunny", temperature: 70 }. Read PAY_TO_ADDRESS from env. Print the configured routes on startup. ``` ### 다음 권장 사항 * [**MCP 서버로 결제하기**](/ko/how-to/pay-with-mcp) — 유료 API를 MCP 도구로 래핑하여 AI 클라이언트가 호출하고 결제할 수 있도록 합니다. * [**빠른 시작**](/ko/tutorial/quick-start) — AI 컨텍스트와 첫 트랜잭션 실행을 5분 만에 함께 진행합니다. * [**Ethereum과의 차이점**](/ko/explanation/ethereum-comparison) — 컨텍스트 블록의 gas 및 USDT0 시맨틱에 대한 심층 분석. ## 컨트랙트 이벤트 인덱싱 인덱싱은 온체인 이벤트를 애플리케이션이 반응할 수 있는 데이터로 변환합니다: 잔액 업데이트, 거래 내역, UI 알림 등이 그 예입니다. 이 가이드에서는 ethers.js를 사용해 배포된 Stable 컨트랙트의 이벤트를 구독하는 방법과, 서비스가 오프라인 상태일 때 발생한 이벤트를 놓치지 않도록 과거 이벤트를 백필하는 방법을 설명합니다. ### 사전 준비 * Stable 테스트넷 또는 메인넷에 배포된 컨트랙트. 필요하다면 [배포](/ko/tutorial/smart-contract)와 [검증](/ko/how-to/verify-contract)을 참고하세요. * Node.js 20 이상. * 컨트랙트 주소와 인덱싱하려는 이벤트의 ABI. ### 1. 설치 및 구성 ```bash npm install ethers ``` ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_TESTNET_WS = "wss://rpc.testnet.stable.xyz"; export const CONTRACT_ADDRESS = "0xDeployedContractAddress"; // Minimal ABI: only the events you want to index. export const CONTRACT_ABI = [ "event NumberUpdated(address indexed caller, uint256 oldValue, uint256 newValue)", ]; ``` ### 2. 실시간 이벤트 구독 WebSocket 프로바이더를 사용하면 검증자가 각 블록을 확정하는 즉시 이벤트를 받을 수 있습니다. WebSocket은 폴링 오버헤드를 피하고 알림 지연 시간을 블록 타임(Stable에서 약 0.7초)에 가깝게 유지합니다. ```typescript // watchLive.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); contract.on("NumberUpdated", (caller, oldValue, newValue, event) => { console.log("NumberUpdated:"); console.log(" caller: ", caller); console.log(" oldValue: ", oldValue.toString()); console.log(" newValue: ", newValue.toString()); console.log(" tx: ", event.log.transactionHash); console.log(" block: ", event.log.blockNumber); }); console.log("Listening for NumberUpdated events..."); ``` ```bash npx tsx watchLive.ts ``` ```text Listening for NumberUpdated events... NumberUpdated: caller: 0x1234...abcd oldValue: 0 newValue: 42 tx: 0x8f3a...2d41 block: 1284371 ``` 호출자가 컨트랙트를 호출하면 이벤트가 실시간으로 도착합니다. ### 3. 과거 이벤트 백필 서비스가 시작될 때는 보통 오프라인 상태일 때 발생한 이벤트를 따라잡아야 합니다. 블록 범위와 함께 `queryFilter`를 사용하세요. ```typescript // backfill.ts import { ethers } from "ethers"; import { STABLE_TESTNET_RPC, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); const latest = await provider.getBlockNumber(); const fromBlock = Math.max(0, latest - 10_000); // last ~10k blocks const events = await contract.queryFilter( contract.filters.NumberUpdated(), fromBlock, latest ); for (const event of events) { console.log( `[block ${event.blockNumber}]`, event.args.caller, "set number to", event.args.newValue.toString() ); } console.log(`Backfilled ${events.length} events from block ${fromBlock} to ${latest}`); ``` ```bash npx tsx backfill.ts ``` ```text [block 1282351] 0x1234...abcd set number to 10 [block 1283092] 0xef01...2345 set number to 25 [block 1284371] 0x1234...abcd set number to 42 Backfilled 3 events from block 1282351 to 1284371 ``` :::warning 넓은 블록 범위(수백만 블록)는 RPC 속도 제한을 초과하고 타임아웃이 발생할 수 있습니다. 프로덕션 인덱서의 경우 10k 블록 단위로 페이지네이션하거나, 인덱싱된 과거 쿼리를 위해 [Stablescan의 Etherscan 호환 API](/ko/how-to/build-p2p-payments#transaction-history)를 사용하세요. ::: ### 4. 인덱싱된 인수로 이벤트 필터링 `indexed` 매개변수가 있는 이벤트(위의 `caller` 등)는 서버 측에서 필터링할 수 있습니다. 모든 이벤트를 읽고 앱에서 필터링하는 대신 필터 값을 전달하세요. ```typescript // watchUser.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); const userAddress = "0x1234...abcd"; const filter = contract.filters.NumberUpdated(userAddress); contract.on(filter, (caller, oldValue, newValue, event) => { console.log(`${caller} set number to ${newValue.toString()}`); }); console.log(`Watching NumberUpdated for ${userAddress}...`); ``` ```bash npx tsx watchUser.ts ``` ```text Watching NumberUpdated for 0x1234...abcd... 0x1234...abcd set number to 42 ``` ### 연결 끊김 처리 WebSocket 연결은 끊길 수 있습니다. 프로덕션 인덱서의 경우 이벤트를 놓치지 않도록 재연결 로직을 구현하세요. ```typescript // resilientWatch.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; let reconnectAttempts = 0; const MAX_RECONNECT = 5; function setupWatcher() { const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); contract.on("NumberUpdated", (caller, oldValue, newValue) => { console.log(`${caller} set number to ${newValue.toString()}`); }); provider.websocket.onerror = (err: any) => { console.error("Provider error:", err); if (reconnectAttempts < MAX_RECONNECT) { reconnectAttempts++; setTimeout(setupWatcher, 5000); } }; } setupWatcher(); ``` ### 다음 추천 * [**언본딩 완료 추적**](/ko/how-to/track-unbonding) — 프로토콜에서 발생하는 시스템 트랜잭션 이벤트(언본딩 완료)를 인덱싱합니다. * [**P2P 결제 앱 구축**](/ko/how-to/build-p2p-payments) — USDT0 Transfer 이벤트에 인덱싱을 적용하고 결제 내역 화면을 구축합니다. * [**JSON-RPC 레퍼런스**](/ko/reference/json-rpc-api) — Stable이 지원하는 `eth_getLogs` 및 관련 메서드를 확인합니다. ## 검증자 데이터 인덱싱 검증자 데이터는 온체인에 존재하며 표준 EVM JSON-RPC로 읽을 수 있습니다. 스테이킹, 슬래싱, 거버넌스 프리컴파일을 통해 현재 상태를 조회하고, 이들의 이벤트 로그로부터 이력을 재구성합니다. 즉, 인덱서나 분석 플랫폼은 노드의 `stabled` CLI나 Cosmos REST에 접근하지 않고도 `eth_call`과 `eth_getLogs`만으로 필요한 모든 것을 읽습니다. :::note **개념:** 스테이킹 모듈이 추적하는 내용과 위임이 작동하는 방식은 [스테이킹 모듈](/ko/explanation/staking-module)을 참조하세요. 메서드별 입력과 출력은 [스테이킹 프리컴파일 참조](/ko/reference/staking-module-api)를 참조하세요. ::: ### 각 데이터 포인트의 출처 | **데이터 포인트** | **출처** | **읽는 방법** | | :--------------- | :---------------------------- | :---------------------------------------------------------------- | | 검증자 이름, 신원, 웹사이트 | 스테이킹 프리컴파일 `validators()` | `description.moniker` 및 관련 필드 | | 스테이크(본딩된 토큰) | 스테이킹 프리컴파일 `validators()` | `tokens` 필드 | | 커미션 | 스테이킹 프리컴파일 `validators()` | `commission` 필드 | | 시간에 따른 스테이크 변화 | 스테이킹 프리컴파일 이벤트 | `Delegate`, `Unbond`, `Redelegate` 로그 | | 참여일 | 스테이킹 프리컴파일 이벤트 | `CreateValidator` 로그 → 블록 타임스탬프 | | 가동 시간 | 슬래싱 프리컴파일 `getSigningInfos()` | `(signedBlocksWindow − missedBlocksCounter) / signedBlocksWindow` | | 투표 이력(집계) | 거버넌스 프리컴파일 `getTallyResult()` | 제안별 집계 | | 투표 이력(검증자별) | 거버넌스 프리컴파일 이벤트 | `Vote`, `VoteWeighted` 로그, voter = operator 주소 | ### 프리컴파일 주소 | **모듈** | **주소** | **용도** | | :----------- | :------------------------------------------- | :------------------------ | | Staking | `0x0000000000000000000000000000000000000800` | 검증자 집합, 스테이크, 커미션, 위임 이벤트 | | Distribution | `0x0000000000000000000000000000000000000801` | 보상 및 커미션 인출 | | Gov | `0x0000000000000000000000000000000000000805` | 제안, 집계, 투표 로그 | | Slashing | `0x0000000000000000000000000000000000000806` | 서명 정보 및 가동 시간 | `https://rpc.stable.xyz`에서 메인넷(Chain ID `988`)에 연결하세요. 엔드포인트와 제한 사항은 [메인넷 정보](/ko/reference/mainnet-information)를 참조하세요. ### 검증자 이름, 스테이크, 커미션 스테이킹 프리컴파일에서 `validators()`를 호출하여 현재 검증자 집합을 읽습니다. 본딩 상태를 전달하여 필터링하세요(예: `BOND_STATUS_BONDED`). 각 항목은 검증자의 `description`(`moniker` 포함), `tokens`(본딩된 스테이크), `commission`을 노출합니다. ```typescript // validators.ts import { createPublicClient, http } from "viem"; const STAKING_PRECOMPILE = "0x0000000000000000000000000000000000000800"; const client = createPublicClient({ transport: http("https://rpc.stable.xyz"), }); // See the staking precompile reference for the full validators() ABI and structs. const validators = await client.readContract({ address: STAKING_PRECOMPILE, abi: stakingAbi, functionName: "validators", args: ["BOND_STATUS_BONDED", { key: "0x", offset: 0n, limit: 100n, countTotal: true, reverse: false }], }); for (const v of validators[0]) { console.log(v.description.moniker, v.tokens.toString(), v.commission.toString()); } ``` ```text StableNode-01 4500000000000000000000000 50000000000000000 StableNode-02 3900000000000000000000000 100000000000000000 ``` `tokens`와 `commission` 값은 18자리 소수로 스케일링되어 있습니다. `commission`을 1e18로 나누면 분수 형태의 비율을 얻을 수 있습니다(예: 5%의 경우 `0.05`). 전체 `Validator` 구조체와 `BOND_STATUS_*` 값은 [스테이킹 프리컴파일 참조](/ko/reference/staking-module-api#validators)를 참조하세요. ### 시간에 따른 스테이크 변화 `validators()`는 스냅샷을 반환합니다. 스테이크가 어떻게 이동했는지 추적하려면 스테이킹 프리컴파일의 위임 이벤트를 인덱싱하세요. `Delegate`, `Unbond`, `Redelegate`는 인덱싱된 `validatorAddr`과 `amount`를 담고 있으므로, 모든 스테이크 변화를 검증자와 블록에 귀속시킬 수 있습니다. ```typescript // stakeChanges.ts import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: STAKING_PRECOMPILE, event: parseAbiItem( "event Delegate(address indexed delegatorAddr, string indexed validatorAddr, uint256 amount, uint256 newShares)" ), fromBlock: 0n, toBlock: "latest", }); console.log(`${logs.length} delegations indexed`); ``` ```text 1842 delegations indexed ``` `Unbond`와 `Redelegate`는 동일한 형태를 따르며 추가로 `completionTime`을 담고 있습니다. 정확한 시그니처는 스테이킹 참조의 [이벤트 섹션](/ko/reference/staking-module-api#events)을 참조하세요. ### 참여일 검증자의 참여일은 해당 검증자의 `CreateValidator` 이벤트의 블록 타임스탬프입니다. 이 이벤트는 검증자 주소로 인덱싱되어 있으므로, 단일 검증자를 필터링하거나 전체 집합을 훑은 다음, `eth_getBlockByNumber`로 각 로그의 `blockNumber`를 타임스탬프로 변환할 수 있습니다. ```typescript // joinDate.ts import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: STAKING_PRECOMPILE, event: parseAbiItem("event CreateValidator(address indexed valiAddr, uint256 value)"), fromBlock: 0n, toBlock: "latest", }); for (const log of logs) { const block = await client.getBlock({ blockNumber: log.blockNumber }); console.log(log.args.valiAddr, new Date(Number(block.timestamp) * 1000).toISOString()); } ``` ```text 0xAbc...123 2025-11-04T09:12:44.000Z 0xDef...456 2025-12-18T17:03:01.000Z ``` :::warning 제네시스 검증자에는 `CreateValidator` 이벤트가 없습니다. 이들은 트랜잭션이 아니라 제네시스 블록에서 생성되었으므로 로그가 존재하지 않습니다. 이들의 참여일은 체인 제네시스인 **2025-10-29**로 취급하세요. 제네시스 이후에 참여한 모든 검증자에 대해 `CreateValidator`를 인덱싱하고, 제네시스 집합은 제네시스 검증자 목록에서 백필하세요. ::: ### 가동 시간 슬래싱 프리컴파일(`0x...806`)에서 `getSigningInfos()`를 사용하여 서명 정보를 읽습니다. 각 레코드는 `signedBlocksWindow`(슬라이딩 윈도우의 크기)와 `missedBlocksCounter`(윈도우 내에서 놓친 블록 수)를 보고합니다. 가동 시간을 다음과 같이 계산하세요: ```text uptime = (signedBlocksWindow − missedBlocksCounter) / signedBlocksWindow ``` `signedBlocksWindow`가 `10000`이고 `missedBlocksCounter`가 `25`인 검증자는 윈도우 동안 99.75%의 가동 시간을 갖습니다. 이는 누적 가동 시간이 아니라 롤링 수치입니다. 가동 시간 이력을 추적하려면 고정된 간격으로 카운터를 스냅샷하고 각 측정값을 저장하세요. :::note 슬래싱 프리컴파일은 Cosmos EVM `x/slashing` 인터페이스를 따릅니다. 해당 주소는 [시스템 모듈 프리컴파일 표](/ko/how-to/use-system-modules#whats-exposed)에 나열되어 있습니다. 정확한 메서드 ABI는 체인의 프리컴파일 인터페이스에서 생성하세요. ::: ### 투표 이력 거버넌스 데이터에는 두 가지 계층이 있습니다. 제안의 집계 결과는 거버넌스 프리컴파일(`0x...805`)에서 `getTallyResult()`를 호출하세요. 누가 무엇에 투표했는지는 `Vote`와 `VoteWeighted` 이벤트 로그를 인덱싱하세요. 이 로그의 voter 주소는 검증자의 operator 주소이므로, 투표를 검증자에 직접 조인할 수 있습니다. ```typescript // votes.ts import { parseAbiItem } from "viem"; const GOV_PRECOMPILE = "0x0000000000000000000000000000000000000805"; const logs = await client.getLogs({ address: GOV_PRECOMPILE, event: parseAbiItem( "event Vote(uint64 indexed proposalId, address indexed voter, uint8 option, uint256 weight)" ), fromBlock: 0n, toBlock: "latest", }); console.log(`${logs.length} votes indexed across all proposals`); ``` ```text 38 votes indexed across all proposals ``` 지금까지의 모든 제안(제안 #1부터 #7까지)에 대해 라이브 투표 로그가 확인되었습니다. 제안별 최종 집계만 필요할 때는 `getTallyResult()`를 사용하고, 검증자별 레코드가 필요할 때는 이벤트 로그를 사용하세요. :::note 거버넌스 프리컴파일은 Cosmos EVM `x/gov` 인터페이스를 따릅니다. 해당 주소는 [시스템 모듈 프리컴파일 표](/ko/how-to/use-system-modules#whats-exposed)에 나열되어 있습니다. 정확한 메서드 ABI와 `VoteOption` 열거형은 체인의 프리컴파일 인터페이스에서 생성하세요. ::: ### 다음 추천 * [**스테이킹 프리컴파일 참조**](/ko/reference/staking-module-api) — 전체 validators(), 위임 메서드, 이벤트 시그니처를 찾아보세요. * [**검증자 생성**](/ko/how-to/run-validator) — 동기화된 노드를 검증자로 등록하여 위 데이터에 나타나게 하세요. * [**인덱서 및 분석**](/ko/reference/indexers) — 이미 정규화된 Stable 데이터를 제공하는 인덱싱 제공자를 둘러보세요. * [**메인넷 정보**](/ko/reference/mainnet-information) — 인덱싱을 시작하기 전에 Chain ID, RPC 엔드포인트, 속도 제한을 확인하세요. 이 가이드는 다양한 플랫폼에서 Stable 노드를 설치하고 설정하기 위한 자세한 지침을 제공합니다. ### 사전 요구 사항 설치를 시작하기 전에 다음 사항을 확인하세요: * 모든 [시스템 요구 사항](/ko/reference/node-system-requirements)을 충족함 * 서버에 대한 root 또는 sudo 액세스 권한 * Linux 명령줄에 대한 기본 지식 ### 설치 방법 플랫폼에 맞는 사전 컴파일된 바이너리를 사용하세요. Stable은 현재 소스에서 빌드하는 것을 지원하지 않습니다. #### 메인넷 ##### Linux AMD64 ```bash # Download the latest binary for AMD64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-amd64-mainnet.tar.gz # Extract the archive tar -xvzf stabled-latest-linux-amd64-mainnet.tar.gz # Move binary to system path sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ##### Linux ARM64 ```bash # Download the binary for ARM64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-arm64-mainnet.tar.gz # Extract and install tar -xvzf stabled-latest-linux-arm64-mainnet.tar.gz sudo mv stabled /usr/bin/ # Verify installation stabled version ``` #### 테스트넷 ##### Linux AMD64 ```bash # Download the latest binary for AMD64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-amd64-testnet.tar.gz # Extract the archive tar -xvzf stabled-latest-linux-amd64-testnet.tar.gz # Move binary to system path sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ##### Linux ARM64 ```bash # Download the binary for ARM64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-arm64-testnet.tar.gz # Extract and install tar -xvzf stabled-latest-linux-arm64-testnet.tar.gz sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ### 노드 초기화 바이너리를 설치한 후 노드를 초기화하세요: #### 1단계: 노드 이름 설정 ```bash # Set your node's moniker (choose a unique name) export MONIKER="your-node-name" ``` #### 2단계: 노드 초기화 #### 메인넷 ```bash # Initialize with the mainnet chain ID stabled init $MONIKER --chain-id stable_988-1 # This creates the configuration directory at ~/.stabled/ ``` > **참고**: chain ID를 포함한 현재 네트워크 매개변수는 [메인넷 정보](/ko/reference/mainnet-information)를 참조하세요 #### 테스트넷 ```bash # Initialize with the testnet chain ID stabled init $MONIKER --chain-id stabletestnet_2201-1 # This creates the configuration directory at ~/.stabled/ ``` > **참고**: chain ID를 포함한 현재 네트워크 매개변수는 [테스트넷 정보](/ko/reference/testnet-information)를 참조하세요 #### 3단계: genesis 파일 다운로드 :::code-group ```bash [Mainnet] # Create backup of default genesis mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup # Download mainnet genesis wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/genesis.zip unzip genesis.zip # Move genesis to config directory cp genesis.json ~/.stabled/config/genesis.json # Verify genesis checksum sha256sum ~/.stabled/config/genesis.json # Expected: e1ceda79a3cc48a1028ca8646a2e9e2d156f610637cfb8b428ca8354277921f1 ``` ```bash [Testnet] # Create backup of default genesis mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup # Download testnet genesis wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/genesis.zip unzip genesis.zip # Move genesis to config directory cp genesis.json ~/.stabled/config/genesis.json # Verify genesis checksum sha256sum ~/.stabled/config/genesis.json # Expected: 66afbb6e57e6faf019b3021de299125cddab61d433f28894db751252f5b8eaf2 ``` ::: #### 4단계: 노드 구성 ##### 구성 파일 다운로드 :::code-group ```bash [Mainnet] # Download optimized configuration (choose one based on your node type) # For RPC/Full nodes: wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/rpc_node_config.zip unzip rpc_node_config.zip # For Archive nodes: # wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/archive_node_config.zip # unzip archive_node_config.zip # Backup original config cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup # Apply new configuration cp config.toml ~/.stabled/config/config.toml # Update moniker in config sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml ``` ```bash [Testnet] # Download optimized configuration wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/rpc_node_config.zip unzip rpc_node_config.zip # Backup original config cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup # Apply new configuration cp config.toml ~/.stabled/config/config.toml # Update moniker in config sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml ``` ::: ##### 필수 구성 업데이트 `~/.stabled/config/app.toml` 편집: ```toml # Enable JSON-RPC for EVM compatibility [json-rpc] enable = true address = "0.0.0.0:8545" ws-address = "0.0.0.0:8546" allow-unprotected-txs = true ``` `~/.stabled/config/config.toml` 편집: :::code-group ```toml [Mainnet] # P2P Configuration [p2p] # Maximum number of peers max_num_inbound_peers = 50 max_num_outbound_peers = 30 # Seed nodes seeds = "9aa181b20248e948567cb47a15eae35d58cd549d@seed1.stable.xyz:46656" # Persistent peers (mainnet seed nodes) persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656" # Enable peer exchange pex = true # RPC Configuration [rpc] # Listen address laddr = "tcp://0.0.0.0:26657" # Maximum number of simultaneous connections max_open_connections = 900 # CORS settings (adjust for production) cors_allowed_origins = ["*"] ``` ```toml [Testnet] # P2P Configuration [p2p] # Maximum number of peers max_num_inbound_peers = 50 max_num_outbound_peers = 30 # Seed nodes seeds = "6f3195823f7e5ee6f911a0a0ceb9ea689e0dc5bd@seed1.testnet.stable.xyz:56656" # Persistent peers (testnet seed nodes) persistent_peers = "128accd3e8ee379bfdf54560c21345451c7048c7@peer1.testnet.stable.xyz:26656" # Enable peer exchange pex = true # RPC Configuration [rpc] # Listen address laddr = "tcp://0.0.0.0:26657" # Maximum number of simultaneous connections max_open_connections = 900 # CORS settings (adjust for production) cors_allowed_origins = ["*"] ``` ::: ### Systemd 서비스 설정 자동 관리를 위해 systemd 서비스를 생성하세요: #### 1단계: 서비스 파일 생성 :::code-group ```bash [Mainnet] sudo tee /etc/systemd/system/stabled.service > /dev/null < /dev/null <> ~/.bashrc echo "export DAEMON_NAME=stabled" >> ~/.bashrc echo "export DAEMON_HOME=$HOME/.stabled" >> ~/.bashrc echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=true" >> ~/.bashrc echo "export DAEMON_RESTART_AFTER_UPGRADE=true" >> ~/.bashrc echo "export DAEMON_LOG_BUFFER_SIZE=512" >> ~/.bashrc echo "export UNSAFE_SKIP_BACKUP=true" >> ~/.bashrc # Load variables source ~/.bashrc ``` #### 3단계: Cosmovisor 디렉터리 구조 설정 ```bash # Create cosmovisor directory structure mkdir -p ~/.stabled/cosmovisor/genesis/bin mkdir -p ~/.stabled/cosmovisor/upgrades # Copy current binary to genesis cp /usr/bin/stabled ~/.stabled/cosmovisor/genesis/bin/ # Create current symlink ln -s ~/.stabled/cosmovisor/genesis ~/.stabled/cosmovisor/current # Verify setup ls -la ~/.stabled/cosmovisor/ cosmovisor run version ``` #### 4단계: 환경 변수 설정 ```bash # Set service name (default: stable) export SERVICE_NAME=stable ``` #### 5단계: 서비스 파일 생성 :::code-group ```bash [Mainnet] sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null < /dev/null < /dev/null < /dev/null <` ### 개요 통합 흐름은 세 단계로 구성됩니다: 1. **InnerTx 빌드**: 사용자가 `gasPrice = 0`으로 트랜잭션에 서명합니다. 2. **Waiver Server에 제출**: 서명된 트랜잭션을 Waiver Server API에 제출합니다. 3. **응답 처리**: waiver 서버가 트랜잭션을 래핑하고 브로드캐스트합니다. 스트리밍된 결과를 처리하고 트랜잭션 해시를 사용자에게 표시합니다. ### 1단계: 사용자의 InnerTx 생성 사용자는 `gasPrice = 0`으로 표준 트랜잭션에 서명합니다. `to` 주소와 메서드 셀렉터는 waiver의 `AllowedTarget` 정책에 의해 허용되어야 합니다. ```typescript // config.ts export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet WAIVER_SERVER: "https://waiver.testnet.stable.xyz", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; ``` ```typescript import { ethers } from "ethers"; import { CONFIG } from "./config"; const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ recipientAddress, ethers.parseUnits("0.01", 18) ]); const gasEstimate = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit: gasEstimate, nonce: nonce, chainId: CONFIG.CHAIN_ID, }; const signedInnerTx = await userWallet.signTransaction(innerTx); ``` :::warning `gasPrice`는 반드시 `0`이어야 합니다. 0이 아니면 waiver 서버가 트랜잭션을 거부합니다. ::: ### 2단계: Waiver Server에 제출 ```typescript import { CONFIG } from "./config"; const API_KEY = process.env.WAIVER_API_KEY; const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTx], }), }); ``` #### 배치 제출 단일 요청으로 여러 개의 서명된 트랜잭션을 제출할 수 있습니다: ```typescript body: JSON.stringify({ transactions: [signedTx1, signedTx2, signedTx3], }) ``` 각 결과 라인에는 배열 내 트랜잭션의 위치에 해당하는 `index` 필드가 포함됩니다. ### 3단계: 응답 처리 응답은 NDJSON(개행으로 구분된 JSON)으로 스트리밍됩니다. 각 라인은 제출된 하나의 트랜잭션에 해당합니다. ```typescript const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); if (result.success) { console.log(`tx ${result.index} confirmed: ${result.txHash}`); } else { console.error(`tx ${result.index} failed: ${result.error.message}`); } } } ``` **성공 응답:** ```json {"index": 0, "id": "abc123", "success": true, "txHash": "0x..."} ``` **실패 응답:** ```json {"index": 1, "id": "def456", "success": false, "error": {"code": "VALIDATION_FAILED", "message": "invalid signature"}} ``` ### 오류 코드 | **코드** | **설명** | | :-------------------- | :--------------------------------- | | `PARSE_ERROR` | 트랜잭션 파싱 실패 | | `INVALID_REQUEST` | 잘못된 형식의 요청 본문 | | `BATCH_SIZE_EXCEEDED` | 배치 크기가 허용된 최대치를 초과함 | | `VALIDATION_FAILED` | 트랜잭션 검증 실패 (예: 잘못된 서명, 허용되지 않은 대상) | | `BROADCAST_FAILED` | 체인으로의 브로드캐스트 실패 | | `RATE_LIMITED` | 속도 제한 초과 | | `QUEUE_FULL` | 서버 큐가 가득 참 | | `TIMEOUT` | 요청 시간 초과 | ### API 레퍼런스 #### GET `/v1/health` 상태 확인 엔드포인트. 인증: 없음. #### POST `/v1/submit` 서명된 inner 트랜잭션의 배치를 제출합니다. 인증: 필수 (Bearer). **요청 본문:** ```json { "transactions": ["0x", "0x"] } ``` 응답은 NDJSON으로 스트리밍됩니다. 각 라인은 제출된 트랜잭션 인덱스에 해당합니다. #### GET `/v1/submit` 스트리밍 제출을 위한 WebSocket 인터페이스. 인증: 필수 (Bearer). ### 핵심 요약 * Gas Waiver는 서버 측 통합입니다. 백엔드가 서명된 사용자 트랜잭션을 Waiver Server에 제출합니다. 사용자는 Waiver Server와 직접 상호작용하지 않습니다. * 사용자가 항상 InnerTx에 서명하므로 서명 무결성이 유지됩니다. waiver는 사용자의 트랜잭션을 수정할 수 없습니다. * 대상 컨트랙트는 waiver의 `AllowedTarget` 목록에 있어야 합니다. ### 다음 추천 * [**제로 가스 트랜잭션**](/ko/how-to/zero-gas-transactions) — 데모 중심의 흐름과 영수증에서 제로 가스를 검증하는 방법을 확인하세요. * [**자체 호스팅 Gas Waiver**](/ko/how-to/self-hosted-gas-waiver) — 호스팅된 API 없이 자체 waiver를 실행하세요. * [**Gas waiver protocol**](/ko/reference/gas-waiver-api) — 전체 래퍼 트랜잭션 사양 및 거버넌스 모델. * [**Stable SDK**](/ko/explanation/sdk-overview) — 타입이 지정된 클라이언트를 사용해 사용자 트랜잭션에 서명한 후 Waiver Server에 제출하세요. Stable 노드를 모니터링하고 일상적인 유지보수 작업을 수행하기 위한 종합 가이드입니다. ### 모니터링 스택 개요 #### 권장 스택 * **Prometheus**: 메트릭 수집 * **Grafana**: 시각화 및 대시보드 * **AlertManager**: 알림 라우팅 및 관리 * **Node Exporter**: 시스템 메트릭 * **Loki**: 로그 집계 (선택 사항) ### 빠른 모니터링 설정 #### 1단계: Prometheus 메트릭 활성화 ```toml # Edit ~/.stabled/config/config.toml [instrumentation] prometheus = true prometheus_listen_addr = ":26660" namespace = "stablebft" ``` 노드 재시작: ```bash sudo systemctl restart ${SERVICE_NAME} ``` #### 2단계: Prometheus 설치 ```bash # Download Prometheus wget https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz tar xvf prometheus-2.45.0.linux-amd64.tar.gz sudo mv prometheus-2.45.0.linux-amd64 /opt/prometheus # Create config sudo tee /opt/prometheus/prometheus.yml > /dev/null < /dev/null < 3 | | `stablebft_consensus_block_interval` | 블록 시간 | > 10s | | `stablebft_p2p_peers` | 연결된 피어 | \< 3 | | `stablebft_mempool_size` | 멤풀 크기 | > 1500 | | `stablebft_mempool_failed_txs` | 실패한 트랜잭션 | > 100/분 | #### 시스템 메트릭 | 메트릭 | 설명 | 알림 임계값 | | ---------------------------------- | ---------- | --------------- | | `node_cpu_seconds_total` | CPU 사용량 | 5분간 > 80% | | `node_memory_MemAvailable_bytes` | 사용 가능한 메모리 | \< 10% | | `node_filesystem_avail_bytes` | 사용 가능한 디스크 | \< 10% | | `node_network_receive_bytes_total` | 네트워크 RX | > 100MB/s | | `node_disk_io_time_seconds_total` | 디스크 I/O | > 80% | | `node_load15` | 시스템 부하 | > CPU 코어 수 \* 2 | ### Grafana 대시보드 설정 #### Stable 대시보드 가져오기 ```json { "dashboard": { "title": "Stable Node Monitoring", "panels": [ { "title": "Block Height", "targets": [ { "expr": "stablebft_consensus_height{chain_id=\"stabletestnet_2201-1\"}" } ] }, { "title": "Peers", "targets": [ { "expr": "stablebft_p2p_peers" } ] }, { "title": "Block Time", "targets": [ { "expr": "rate(stablebft_consensus_height[1m]) * 60" } ] }, { "title": "Mempool Size", "targets": [ { "expr": "stablebft_mempool_size" } ] } ] } } ``` #### 커스텀 대시보드 가져오기 Grafana UI를 통해 대시보드를 가져옵니다: ```bash # Navigate to Dashboards > Import > Upload JSON file # Or use Dashboard ID in Grafana's dashboard library ``` ### AlertManager 설정 #### AlertManager 설치 ```bash # Download AlertManager wget https://github.com/prometheus/alertmanager/releases/download/v0.26.0/alertmanager-0.26.0.linux-amd64.tar.gz tar xvf alertmanager-0.26.0.linux-amd64.tar.gz sudo mv alertmanager-0.26.0.linux-amd64 /opt/alertmanager # Configure sudo tee /opt/alertmanager/alertmanager.yml > /dev/null < 1500 for: 10m labels: severity: warning annotations: summary: "High mempool size: {{ $value }}" - alert: DiskSpaceLow expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1 for: 5m labels: severity: critical annotations: summary: "Low disk space: {{ $value | humanizePercentage }}" - alert: HighCPUUsage expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 10m labels: severity: warning annotations: summary: "High CPU usage: {{ $value }}%" ``` ### 로그 모니터링 #### Systemd 로그 ```bash # View recent logs sudo journalctl -u ${SERVICE_NAME} -n 100 # Follow logs sudo journalctl -u ${SERVICE_NAME} -f # Filter by time sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" # Export logs sudo journalctl -u ${SERVICE_NAME} --since today > stable-logs-$(date +%Y%m%d).log ``` #### 로그 분석 스크립트 ```bash #!/bin/bash # analyze-logs.sh # Count errors in last hour echo "Errors in last hour:" sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -c ERROR # Show peer connections echo "Peer connections:" sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep "Peer connection" | tail -10 # Check for consensus issues echo "Consensus rounds:" sudo journalctl -u ${SERVICE_NAME} --since "30 minutes ago" | grep -E "enterNewRound|Timeout" | tail -20 # Memory usage patterns echo "Memory warnings:" sudo journalctl -u ${SERVICE_NAME} --since "1 day ago" | grep -i memory ``` #### Loki 설정 (선택 사항) ```bash # Install Loki wget https://github.com/grafana/loki/releases/download/v2.9.0/loki-linux-amd64.zip unzip loki-linux-amd64.zip sudo mv loki-linux-amd64 /usr/local/bin/loki # Install Promtail wget https://github.com/grafana/loki/releases/download/v2.9.0/promtail-linux-amd64.zip unzip promtail-linux-amd64.zip sudo mv promtail-linux-amd64 /usr/local/bin/promtail # Configure Promtail sudo tee /etc/promtail-config.yml > /dev/null < ~/reports/daily_$(date +%Y%m%d).log curl -s localhost:26657/status | jq >> ~/reports/daily_$(date +%Y%m%d).log ``` #### 주간 유지보수 ```bash #!/bin/bash # weekly-maintenance.sh # Prune old data stabled prune # Compact database stabled compact # Update peer list wget https://raw.githubusercontent.com/stable-chain/networks/main/testnet/peers.txt cat peers.txt >> ~/.stabled/config/config.toml # Create snapshot (optional) ./create-snapshot.sh # System updates sudo apt update sudo apt upgrade -y # Restart node (during low activity) sudo systemctl restart ${SERVICE_NAME} ``` #### 데이터베이스 유지보수 ```bash # Check database size du -sh ~/.stabled/data/ # Analyze database stabled debug db stats ~/.stabled/data ``` ### 성능 모니터링 #### 리소스 사용량 추적 ```bash #!/bin/bash # track-resources.sh while true; do TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') CPU=$(top -bn1 | grep "stabled" | awk '{print $9}') MEM=$(top -bn1 | grep "stabled" | awk '{print $10}') IO=$(iostat -x 1 2 | tail -n2 | awk '{print $14}') echo "$TIMESTAMP,CPU:$CPU,MEM:$MEM,IO:$IO" >> ~/metrics/resources.csv sleep 60 done ``` #### 쿼리 성능 ```bash # Monitor RPC response times while true; do START=$(date +%s%N) curl -s http://localhost:26657/status > /dev/null END=$(date +%s%N) DIFF=$((($END - $START) / 1000000)) echo "RPC response time: ${DIFF}ms" sleep 5 done ``` ### 모니터링 모범 사례 1. **중복 모니터링 설정** * 외부 모니터링 서비스 사용 * 크로스 노드 모니터링 구현 * 데드맨 스위치 알림 설정 2. **알림 피로 방지** * 기준치를 기반으로 알림 임계값 조정 * 알림 그룹화 및 억제 사용 * 에스컬레이션 정책 구현 3. **데이터 보존** * 메트릭을 최소 30일간 유지 * 중요한 로그 아카이브 * 모니터링 설정의 정기 백업 4. **보안** * 강력한 비밀번호로 Grafana 보호 * 모든 엔드포인트에 HTTPS 사용 * prometheus 접근 제한 5. **문서화** * 모든 커스텀 메트릭 문서화 * 알림에 대한 런북 유지 * 대시보드 설명 최신 상태 유지 ### 다음 단계 * 문제 해결을 위해 [문제 해결 가이드 검토](/ko/how-to/troubleshoot-node) * 모니터링을 포함한 [업그레이드 설정](/ko/how-to/upgrade-node) * 요구 사항에 맞는 커스텀 알림 설정 ## 인보이스로 결제하기 이 가이드는 인보이스 메타데이터에서 파생된 결정적 nonce를 사용하여 [ERC-3009](/ko/explanation/erc-3009)로 온체인에서 인보이스를 정산하는 과정을 안내합니다. nonce는 각 결제를 해당 인보이스에 연결하고 이중 결제를 방지합니다. :::note **개념:** 인보이스 정산 모델과 전통적인 B2B 인보이스 발행과의 비교는 [인보이스 정산](/ko/reference/invoices)을 참조하세요. ::: ### 무엇을 만들게 되나요 전체 인보이스 라이프사이클: 구매자가 오프체인에서 ERC-3009 인가에 서명하고, 공급업체가 이를 온체인에 제출하며, 대사 처리는 결과로 생성된 `AuthorizationUsed` 이벤트를 결정적 nonce로 인보이스에 다시 매칭합니다. #### 데모 ```text step 1. Invoice issued number: INV-2026-001234 amount: 5000 USDT0 dueDate: 2026-04-30 step 2. Buyer signs authorization (off-chain, no gas) nonce: 0xa1b2...c3d4 (from invoice metadata) signature: 0xf0e9...1234 step 3. Vendor submits transferWithAuthorization tx: 0x8f3a...2d41 amount: 5000 USDT0 transferred to vendor step 4. Reconciliation AuthorizationUsed(nonce=0xa1b2...) → invoice INV-2026-001234 Transfer event verified for correct amount and parties ERP: marked PAID at block 1284371 ``` ### 개요 **구매자:** ``` ─── Buyer ─────────────────────────────────────────── nonce = getInvoiceNonce(invoice) authorization = { from: buyer, to: vendor, value: amount, nonce, ... } signature = signTypedData(authorization) // Option A: Buyer submits the transaction directly. usdt0.transferWithAuthorization(authorization, signature) // Option B: Buyer sends {authorization, signature} to the vendor. // The vendor (or a facilitator) submits on the buyer's behalf. ``` **공급업체:** ``` ─── Vendor ────────────────────────────────────────── // If Option B: submit transferWithAuthorization using the buyer's signature // Reconcile via AuthorizationUsed event on AuthorizationUsed(authorizer, nonce): invoice = nonceToInvoice.get(nonce) transferLog = receipt.logs.find(Transfer matching invoice.buyer, invoice.vendor, invoice.amount) if transferLog: erpSystem.markPaid(invoice.id, txHash, settledAt) ``` ### 구성 ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const EIP712_DOMAIN = { name: "USDT0", version: "1", chainId: CHAIN_ID, verifyingContract: USDT0_ADDRESS, }; export const TRANSFER_WITH_AUTHORIZATION_TYPE = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; export interface Invoice { number: string; // e.g. "INV-2026-001234" vendor: string; // vendor wallet address buyer: string; // buyer wallet address amount: bigint; // amount in USDT0 atomic units (6 decimals) dueDate: number; // Unix timestamp } ``` ### 1단계: 결정적 nonce 생성 구매자와 공급업체 모두 인보이스 메타데이터로부터 동일한 nonce를 독립적으로 계산할 수 있습니다. 외부 레지스트리가 필요하지 않습니다. ```typescript // nonce.ts import { ethers } from "ethers"; import { Invoice } from "./config"; export function getInvoiceNonce(invoice: Invoice): string { return ethers.solidityPackedKeccak256( ["string", "address", "address", "uint256", "uint256"], [ invoice.number, invoice.vendor, invoice.buyer, invoice.amount, invoice.dueDate, ] ); } // Example const invoice: Invoice = { number: "INV-2026-001234", vendor: "0xVendorAddress", buyer: "0xBuyerAddress", amount: ethers.parseUnits("5000", 6), // 5,000 USDT0 dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000), }; const nonce = getInvoiceNonce(invoice); // Same input always produces the same nonce. // This nonce is consumed on-chain upon payment, preventing double payment. ``` ### 2단계: 인가에 서명 (구매자) 구매자는 1단계의 결정적 nonce를 사용하여 ERC-3009 `transferWithAuthorization`에 서명합니다. ```typescript // sign-invoice.ts import { ethers } from "ethers"; import { provider, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPE, Invoice, } from "./config"; import { getInvoiceNonce } from "./nonce"; const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider); async function signInvoiceAuthorization(invoice: Invoice) { const nonce = getInvoiceNonce(invoice); const gracePeriod = 30 * 24 * 60 * 60; // 30 days after due date const authorization = { from: invoice.buyer, to: invoice.vendor, value: invoice.amount, validAfter: 0, validBefore: invoice.dueDate + gracePeriod, nonce, }; const signature = await buyerWallet.signTypedData( EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPE, authorization ); return { authorization, signature }; } ``` ### 3단계: 트랜잭션 제출 제출하는 주체에 따라 두 가지 옵션이 있습니다. #### 옵션 A: 구매자가 제출 구매자가 `transferWithAuthorization` 트랜잭션을 직접 제출하고 가스를 지불합니다. 구매자가 결제 실행 시점과 방법을 제어할 때 사용합니다. 예를 들어 구매자의 회계 시스템이 내부 승인 흐름에 연결된 tx 해시를 필요로 하는 경우입니다. ```typescript // pay.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider); const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", ], buyerWallet, ); async function payInvoice( authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string }, signature: string, ) { const { v, r, s } = ethers.Signature.from(signature); const tx = await usdt0.transferWithAuthorization( authorization.from, authorization.to, authorization.value, authorization.validAfter, authorization.validBefore, authorization.nonce, v, r, s, ); const receipt = await tx.wait(1); console.log("Invoice paid, tx:", receipt.hash); // The nonce is now consumed; the same invoice cannot be paid twice. return { txHash: receipt.hash, blockNumber: receipt.blockNumber }; } ``` #### 옵션 B: 공급업체가 제출 구매자는 API, 이메일 또는 어떤 채널을 통해서든 `{authorization, signature}`를 공급업체에 보냅니다. 공급업체(또는 퍼실리테이터)가 구매자를 대신하여 트랜잭션을 제출하므로 구매자는 가스를 관리할 필요가 없습니다. 공급업체가 동일한 요청 흐름 내에서 동기 확인을 필요로 할 때 사용합니다. ```typescript // settle.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const vendorWallet = new ethers.Wallet(process.env.VENDOR_KEY!, provider); const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", ], vendorWallet, ); async function settleInvoice( authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string }, signature: string, ) { const { v, r, s } = ethers.Signature.from(signature); const tx = await usdt0.transferWithAuthorization( authorization.from, authorization.to, authorization.value, authorization.validAfter, authorization.validBefore, authorization.nonce, v, r, s, ); const receipt = await tx.wait(1); console.log("Invoice settled, tx:", receipt.hash); return { txHash: receipt.hash, blockNumber: receipt.blockNumber }; } ``` ### 4단계: 온체인 이벤트를 통한 대사 처리 (공급업체) 누가 트랜잭션을 제출했든 관계없이, 모든 인보이스 결제는 결정적 nonce를 담은 `AuthorizationUsed` 이벤트를 발생시킵니다. 공급업체는 이 이벤트를 수신하고 nonce로 대기 중인 인보이스에 매칭합니다. nonce가 인보이스 메타데이터에서 파생되므로 매칭은 정확합니다. :::note nonce로 매칭하면 어떤 인보이스가 결제되었는지 식별할 수 있지만, 공급업체는 동일한 트랜잭션 내의 `Transfer` 이벤트도 검증하여 올바른 금액이 올바른 수취인에게 전송되었는지 확인해야 합니다. 아래 코드는 이 검증을 포함합니다. ::: ```typescript // reconcile.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS, Invoice } from "./config"; import { getInvoiceNonce } from "./nonce"; const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)", "event Transfer(address indexed from, address indexed to, uint256 value)", ], provider, ); // Build a lookup map: nonce -> invoice. // In production, this comes from your invoice database. const invoices: Invoice[] = [ { number: "INV-2026-001234", vendor: "0xVendorAddress", buyer: "0xBuyerAddress", amount: ethers.parseUnits("5000", 6), dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000), }, ]; const nonceToInvoice = new Map(); for (const inv of invoices) { nonceToInvoice.set(getInvoiceNonce(inv), inv); } usdt0.on("AuthorizationUsed", async (authorizer: string, nonce: string, event: any) => { const invoice = nonceToInvoice.get(nonce); if (!invoice) return; // not one of our invoices const receipt = await event.getTransactionReceipt(); const transferLog = receipt.logs .map((log: any) => { try { return usdt0.interface.parseLog(log); } catch { return null; } }) .find( (parsed: any) => parsed?.name === "Transfer" && parsed.args[0].toLowerCase() === invoice.buyer.toLowerCase() && parsed.args[1].toLowerCase() === invoice.vendor.toLowerCase() && parsed.args[2] === invoice.amount ); if (!transferLog) { console.error("No matching Transfer event for invoice:", invoice.number); return; } // All checks passed console.log(`Invoice ${invoice.number} PAID`); console.log(" tx:", receipt.hash); console.log(" settled at block:", receipt.blockNumber); // In production: update your ERP/accounting system here // erpSystem.markPaid(invoice.number, receipt.hash, receipt.blockNumber); }); console.log("Listening for invoice settlements..."); ``` ```bash npx tsx reconcile.ts ``` ```text Listening for invoice settlements... Invoice INV-2026-001234 PAID tx: 0x8f3a...2d41 settled at block: 1284371 ``` ### 실패한 결제 처리 제출된 `transferWithAuthorization`은 여러 가지 이유로 되돌려질(revert) 수 있습니다. 각각을 감지하여 공급업체나 구매자에게 표시함으로써 인보이스를 재시도하거나 마감할 수 있도록 합니다. | **Revert 사유** | **원인** | **복구 방법** | | :----------------------------------------------- | :----------------------------------- | :---------------------------------------- | | `FiatTokenV2: invalid signature` | 서명이 인가 필드와 일치하지 않습니다. | 인보이스 데이터를 변경하지 않은 상태로 구매자에게 재서명을 요청하세요. | | `FiatTokenV2: authorization is used or canceled` | nonce가 이미 소비되었거나(이중 제출) 구매자가 취소했습니다. | 인보이스를 이미 결제됨으로 표시하고, nonce로 원래 tx를 조회하세요. | | `FiatTokenV2: authorization is not yet valid` | `validAfter` 이전에 제출되었습니다. | `validAfter`까지 기다리거나 새 인가를 발급하세요. | | `FiatTokenV2: authorization is expired` | `validBefore` 이후에 제출되었습니다. | 확장된 기간으로 새 인가를 발급하세요. | | `FiatTokenV2: transfer amount exceeds balance` | 구매자의 USDT0 잔액이 부족합니다. | 구매자에게 지갑에 자금을 채우도록 알린 후 동일한 서명으로 재시도하세요. | revert를 포착하고 재시도하기 전에 분류하세요. ```typescript // retry.ts import { ethers } from "ethers"; async function submitWithRetry( submit: () => Promise, ): Promise { try { const tx = await submit(); const receipt = await tx.wait(1); return receipt!.hash; } catch (err: any) { const reason = err?.info?.error?.message || err?.reason || err?.message || ""; if (reason.includes("authorization is used or canceled")) { // Lookup the original tx by AuthorizationUsed event; mark invoice paid. throw new Error("ALREADY_PAID"); } if (reason.includes("authorization is expired")) { throw new Error("AUTHORIZATION_EXPIRED"); } if (reason.includes("invalid signature")) { throw new Error("INVALID_SIGNATURE"); } if (reason.includes("transfer amount exceeds balance")) { throw new Error("INSUFFICIENT_BALANCE"); } throw err; } } ``` :::warning 오류를 분류하지 않고 실패한 제출을 절대 재시도하지 마세요. 되돌려진 transferWithAuthorization에 대한 무분별한 재시도는 구매자가 잔액을 충전한 후 검증을 통과할 수 있으며, 이는 구매자의 최신 의도와 일치하지 않을 수 있습니다. ::: ### 다음 추천 * [**인보이스 정산 개념**](/ko/reference/invoices) — 결정적 nonce 대사 처리 모델을 이해하세요. * [**ERC-3009**](/ko/explanation/erc-3009) — 이 흐름의 기반이 되는 서명된 인가 표준을 검토하세요. * [**가스 없는 트랜잭션 활성화**](/ko/how-to/integrate-gas-waiver) — Gas Waiver와 결합하여 정산 경로에서 가스를 제거하세요. ## MCP 서버로 결제하기 이 가이드는 x402가 활성화된 API를 [MCP](https://modelcontextprotocol.io) 도구에 연결하여 AI 클라이언트가 자연어 프롬프트를 통해 이를 호출하고 비용을 지불할 수 있도록 하는 방법을 보여줍니다. 이는 [호출당 결제 API 구축하기](/ko/how-to/build-pay-per-call)의 서버를 기반으로 합니다. ### 구축할 내용 x402로 결제하는 엔드포인트를 도구로 감싸는 MCP 서버입니다. AI 클라이언트가 자연어 프롬프트를 입력하면 각 도구 호출이 x402 결제 요청을 트리거하고, 정산은 Stablescan에서 확인할 수 있습니다. 사용자는 지갑 프롬프트를 전혀 보지 않습니다. #### 데모 ```text step 1. User in Claude: "Pull financials for ACME Corp and assess credit risk." step 2. Client calls get_company_financials("ACME") → MCP handler: fetchWithPayment("/financials?ticker=ACME") → 402 Payment Required → sign ERC-3009 → retry → Facilitator settles $0.01 USDT0 on-chain → tx: 0x8f3a...aaaa → 200 OK { revenue, debt_ratio, cash_flow } step 3. Client calls assess_credit_risk(financials) → MCP handler: fetchWithPayment("/credit-risk", POST) → Facilitator settles $0.05 USDT0 on-chain → tx: 0x9bc4...bbbb → 200 OK { score: 72, rating: "moderate" } step 4. Claude responds: "ACME Corp has a credit risk score of 72 (moderate). Revenue is stable but debt-to-equity ratio is elevated at 1.8x..." ``` 두 `tx` 값 모두 [https://stablescan.xyz](https://stablescan.xyz)에서 확인할 수 있습니다. :::note **에이전트 지갑 자금 충전**: MCP 서버는 사용자가 제어하는 시드 문구로 결제에 서명합니다. 서버를 시작하기 전에 해당 지갑에 메인넷의 USDT0를 충전하세요. `$0.10` 이상의 잔액이면 여러 번의 유료 호출을 처리할 수 있으며, `$1.00`면 확장 테스트에 충분합니다. 필요에 따라 지갑 주소로 표준 USDT0 전송을 사용해 충전하세요. ::: ### 개요 **MCP 서버:** ```typescript // --- MCP Server --- // Bridge x402-enabled APIs to MCP tools tools = { "get_company_financials": { handler: (ticker) => fetchWithPayment("https://api.example.com/financials?ticker=" + ticker), }, "assess_credit_risk": { handler: (financials) => fetchWithPayment("https://api.example.com/credit-risk", { method: "POST", body: JSON.stringify({ financials }), }), }, } ``` **사용자 (AI 클라이언트를 통해):** ``` ─── AI Client ─────────────────────────────────────── User: "Pull financials for ACME Corp and assess their credit risk." Client calls get_company_financials tool → MCP server sends x402 paid request → Facilitator settles USDT0 on-chain → API returns financial data Client calls assess_credit_risk tool with the result → MCP server sends x402 paid request → Facilitator settles USDT0 on-chain → API returns risk assessment → Client responds with the combined result ``` ### 사전 준비 * 실행 중인 x402 서버 ([호출당 결제 API 구축하기](/ko/how-to/build-pay-per-call) 참고). * MCP 호환 AI 클라이언트 (Claude Desktop, Claude Code 등). ### 1단계: MCP 서버 생성하기 MCP 서버는 AI 클라이언트와 x402가 활성화된 API 사이의 브리지 역할을 합니다. 각 도구는 x402 클라이언트 SDK를 사용해 유료 요청을 보내고 결과를 반환합니다. ```bash npm install @modelcontextprotocol/sdk @x402/fetch @x402/evm @tetherto/wdk-wallet-evm ``` ```typescript // mcp-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; import { z } from "zod"; // --- Wallet and x402 client --- const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, { provider: "https://rpc.stable.xyz", }).getAccount(0); const client = new x402Client(); registerExactEvmScheme(client, { signer: account }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); // --- x402 API base URL --- const API_BASE = process.env.API_BASE || "http://localhost:4021"; // --- MCP server --- const server = new McpServer({ name: "x402-payments", version: "1.0.0", }); server.tool( "get_company_financials", "Get company financial data by ticker (paid endpoint, $0.01 per call)", { ticker: z.string().describe("Company ticker symbol (e.g. ACME)") }, async ({ ticker }) => { const response = await fetchWithPayment(`${API_BASE}/financials?ticker=${ticker}`); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; }, ); server.tool( "assess_credit_risk", "Assess credit risk from financial data (paid endpoint, $0.05 per call)", { financials: z.string().describe("JSON string of company financial data") }, async ({ financials }) => { const response = await fetchWithPayment(`${API_BASE}/credit-risk`, { method: "POST", headers: { "Content-Type": "application/json" }, body: financials, }); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; }, ); server.tool( "check_balance", "Check the USDT0 balance of the payment wallet", {}, async () => { const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const balance = await account.getTokenBalance(USDT0_STABLE); const formatted = (Number(balance) / 1e6).toFixed(2); return { content: [{ type: "text", text: `Wallet balance: ${formatted} USDT0` }], }; }, ); // --- Start --- const transport = new StdioServerTransport(); await server.connect(transport); ``` 각 도구 핸들러는 `fetchWithPayment`를 호출하며, 이는 전체 x402 결제 주기를 자동으로 처리합니다. AI 클라이언트는 도구 이름, 설명, 매개변수만 봅니다. ### 2단계: AI 클라이언트 설정하기 AI 클라이언트의 설정에 MCP 서버를 추가합니다. **Claude Desktop** (`claude_desktop_config.json`): ```json { "mcpServers": { "x402-payments": { "command": "npx", "args": ["tsx", "/path/to/mcp-server.ts"], "env": { "SEED_PHRASE": "your seed phrase here", "API_BASE": "https://api.example.com" } } } } ``` **Claude Code:** ```bash claude mcp add x402-payments -- npx tsx /path/to/mcp-server.ts ``` 설정 후 AI 클라이언트를 재시작하세요. 도구들이 사용 가능한 도구 목록에 나타나야 합니다. :::warning MCP 설정의 시드 문구는 실제 자금을 제어합니다. 평문 설정 파일에 저장하는 대신 OS 키체인이나 시크릿 매니저를 사용해 안전하게 보관하세요. ::: ### 3단계: 프롬프트 입력 및 사용하기 설정이 완료되면 AI 클라이언트는 사용자의 프롬프트를 통해 유료 API를 호출할 수 있습니다: **사용자:** "ACME Corp의 재무 정보를 가져와서 신용 위험을 평가해줘." 1. 클라이언트가 `get_company_financials("ACME")`를 호출: x402를 통해 $0.01 결제. 매출, 부채 비율, 현금 흐름 등을 반환합니다. 2. 클라이언트가 `assess_credit_risk(financials)`를 호출: x402를 통해 $0.05 결제. 위험 점수, 등급, 핵심 요인을 반환합니다. 3. 클라이언트 응답: "ACME Corp의 신용 위험 점수는 72점(중간)입니다. 매출은 안정적이지만 부채비율이 1.8배로 다소 높습니다..." 개별 도구도 단독으로 작동합니다: * "ACME Corp의 재무 정보를 가져와줘"는 `get_company_financials`를 호출합니다 ($0.01). * "이 데이터의 신용 위험을 평가해줘"는 `assess_credit_risk`를 호출합니다 ($0.05). * "USDT0가 얼마나 남았어?"는 `check_balance`를 호출합니다. 사용자는 지갑, 서명, 결제 흐름과 상호작용하지 않습니다. MCP 서버가 각 도구 호출에 대한 결제를 투명하게 처리합니다. ### 지출 제어 예기치 않은 지출을 방지하려면 MCP 서버에 제어 장치를 추가하는 것을 고려하세요. ```typescript const MAX_PER_CALL = 100_000; // $0.10 in base units const MAX_PER_SESSION = 5_000_000; // $5.00 in base units let sessionSpent = 0n; function checkSpendingLimit(amount: bigint) { if (amount > BigInt(MAX_PER_CALL)) { throw new Error(`Amount exceeds per-call limit of $${MAX_PER_CALL / 1e6}`); } if (sessionSpent + amount > BigInt(MAX_PER_SESSION)) { throw new Error(`Session spending limit of $${MAX_PER_SESSION / 1e6} reached`); } sessionSpent += amount; } ``` 이러한 제한은 서버 측에서 실행됩니다. AI 클라이언트는 이를 수정하거나 우회할 수 없습니다. ### 다음 추천 * [**호출당 결제 API 구축하기**](/ko/how-to/build-pay-per-call) — 이 MCP 서버가 브리지하는 x402 서버를 설정합니다. * [**x402 개념**](/ko/explanation/x402) — 이러한 결제 뒤에 있는 정산 프로토콜을 살펴봅니다. * [**AI로 개발하기**](/ko/how-to/develop-with-ai) — Stable의 Docs 및 Runtime MCP 서버를 동일한 AI 클라이언트에 연결합니다. ## 프로덕션 준비 상태 테스트넷에서 메인넷으로 전환하기 전에 아래 각 섹션을 순서대로 검토하세요. ### 출시 전 확인사항 * **네트워크 대상.** 애플리케이션이 테스트넷이 아닌 메인넷 값을 읽습니다: chain ID `988`, RPC `https://rpc.stable.xyz`, 익스플로러 `https://stablescan.xyz`. 전체 구성은 [Connect](/ko/reference/connect)에 있습니다. * **컨트랙트 검증.** 배포된 컨트랙트가 [stablescan.xyz](https://stablescan.xyz)에서 검증되어 사용자와 파트너가 검사할 수 있습니다. * **메인넷 자금 조달 경로.** 프로덕션 지갑이 USDT0를 확보할 수 있는 문서화된 방법이 있습니다: 직접, LayerZero를 통한 브리지, 또는 커스터디언. 파우셋은 테스트넷 전용입니다. * **환경 격리.** 키, RPC 자격 증명, 서명 경로가 테스트넷과 메인넷 간에 분리되어 있습니다. ### 보안 점검 USDT0의 이중 역할 동작은 이더리움에서 가져온 몇 가지 가정을 무너뜨립니다. 아래 각 항목을 검증해야 합니다. 전체 목록은 [마이그레이션 체크리스트](/ko/explanation/usdt0-behavior)에 있습니다. **지급 능력(solvency) 점검은 미러가 아닌 실제 네이티브 잔액을 읽습니다.** :::warning 입금된 네이티브 값을 내부 변수에서 추적하는 것은 안전하지 않습니다. 외부 `USDT0.transferFrom` 호출은 어떤 컨트랙트 코드도 실행하지 않고 컨트랙트의 네이티브 잔액을 고갈시킬 수 있습니다. ::: ```solidity // SAFE — checks real balance at the moment of transfer function withdraw() external { uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount, "insufficient balance"); payable(msg.sender).call{value: amount}(""); } ``` **Allowance 기반 고갈 경로가 테스트로 다뤄집니다.** 모든 `approve` / `transferFrom` / `permit` 경로에는 컨트랙트의 네이티브 잔액 고갈을 시도하는 테스트가 있습니다. **제로 주소 전송은 호출 전에 거부됩니다.** :::warning Stable에서는 네이티브 및 ERC-20 전송 모두 `address(0)`로의 전송 시 revert됩니다. 수신자를 명시적으로 검증하세요. 그렇지 않으면 트랜잭션이 실패합니다. ::: ```solidity require(recipient != address(0), "zero address recipient"); payable(recipient).call{value: amount}(""); ``` **주소 재사용 감지는 `EXTCODEHASH`에 의존하지 않습니다.** Permit 기반 승인은 nonce 증가 없이 네이티브 잔액을 변경하므로 `EXTCODEHASH`가 제로 해시와 빈 해시 사이를 오갈 수 있습니다. 대신 명시적 추적을 사용하세요. ### 성능 및 신뢰성 * **RPC 이중화.** 프로덕션 트래픽에는 페일오버 계획이 있습니다. 서드파티 제공업체는 [RPC 제공업체](/ko/reference/rpc-providers)에 나열되어 있습니다. * **가스 추정.** 트랜잭션은 `maxPriorityFeePerGas`를 `0`으로 설정하고 현재 base fee로부터 `maxFeePerGas`를 계산합니다. [가스 가격 책정](/ko/reference/gas-pricing-api)을 참조하세요. * **블록 시간.** 블록은 약 0.7초마다 생성되며 단일 슬롯 최종성을 가집니다. 폴링 간격과 확인 임계값을 이 주기에 맞춰 조정합니다. * **재시도.** 일시적인 RPC 오류는 멱등적으로 재시도됩니다. 금전적으로 민감한 플로우의 경우 다운스트림 상태 변경 전에 영수증 또는 로그를 통해 포함 여부를 검증합니다. ### 운영 소유권 * **모니터링.** 자체 노드를 운영하는 경우 알림이 블록 생성, 피어 상태, RPC 지연 시간을 감시합니다. [모니터링](/ko/how-to/monitor-node)을 참조하세요. 서드파티 RPC를 사용하는 경우 제공업체 SLA와 페일오버 텔레메트리를 추적하세요. * **업그레이드.** 노드 운영자가 업그레이드를 일정에 맞춰 진행할 수 있도록 프로토콜 릴리스를 추적합니다. [메인넷 버전 히스토리](/ko/reference/mainnet-version-history)를 참조하세요. * **런북.** 컨트랙트 일시 중지, 키 교체, RPC 제공업체 전환에 대한 롤백 절차가 존재합니다. ### 지원 및 에스컬레이션 * [개발자 지원](/ko/reference/developer-assistance): FAQ 및 참조 자료 포인터. * [Discord](https://discord.gg/stablexyz): 커뮤니티 지원 및 프로토콜 업데이트. * `bizdev@stable.xyz`: 파트너십 및 통합 관련 논의. ### 다음 권장 사항 * [**USDT0 동작**](/ko/explanation/usdt0-behavior) — 전체 마이그레이션 체크리스트와 컨트랙트 설계 요구사항을 읽어보세요. * [**메인넷 정보**](/ko/reference/mainnet-information) — 메인넷 체인 매개변수와 버전 히스토리를 확인하세요. * [**RPC 제공업체**](/ko/reference/rpc-providers) — 이중화를 위한 서드파티 RPC 제공업체를 선택하세요. * [**모니터링**](/ko/how-to/monitor-node) — 블록 생성 및 RPC 상태에 대한 메트릭과 알림을 연결하세요. 검증자는 온체인에 등록되고 스테이크를 본딩한 동기화된 풀 노드입니다. 먼저 노드를 설치하고 동기화한 다음, 스테이킹 프리컴파일(`0x0000000000000000000000000000000000000800`)에서 `createValidator`를 호출하여 등록합니다. 이 페이지에서는 등록 단계를 다룹니다. 노드 자체에 대해서는 [노드 설치하기](/ko/how-to/install-node)와 [노드 구성](/ko/reference/node-configuration)을 참조하세요. :::warning 노드가 완전히 동기화된 후에만 등록하세요. 따라잡기 전에 서명하는 검증자는 이중 서명을 하여 세트에서 영구적으로 제거(tombstone)될 수 있습니다. 시작하기 전에 `config.toml`에서 `double_sign_check_height = 2` 또는 그 이상으로 설정하세요(자세한 내용은 [노드 구성](/ko/reference/node-configuration#consensus-configuration) 참조). `1`로 설정하면 검사를 수행하지 않습니다. ::: ### 사전 요구사항 * 메인넷(Chain ID `988`)에서 완전히 동기화된 풀 노드. [노드 설치하기](/ko/how-to/install-node) 참조. * `~/.stabled/config/config.toml`에서 `double_sign_check_height`가 `2` 또는 그 이상으로 설정됨. * 프리컴파일 호출에 사용되는 `cast`를 위한 [Foundry](https://book.getfoundry.sh/) 설치. * 검증자의 EVM 주소에 USDT0로 입금된 스테이킹 금액. 진행하기 전에 노드가 따라잡았는지 확인하세요. `catching_up`은 반드시 `false`여야 합니다. ```bash curl -s localhost:26657/status | jq '.result.sync_info.catching_up' ``` ```text false ``` ### 1단계: 검증자 키 준비 오퍼레이터 계정을 생성한 다음, `createValidator`에 필요한 두 가지 값인 합의 공개 키(base64)와 검증자의 EVM 주소를 읽습니다. ```bash # Create the validator operator account stabled keys add validator # Consensus public key (base64) — save this stabled comet show-validator | jq .key # Derive the validator's EVM address (0x form) stabled keys parse $(stabled keys show validator -a) ``` ```text "AbCd...base64PubKey...==" # ... # then, evm address is 0xCAEA59C7476C87D0FF6BE6F04DA207601D5BE7D0 ``` :::warning `~/.stabled/config/priv_validator_key.json`을 오프라인으로 백업하고, 동일한 키로 두 개의 노드를 절대 실행하지 마세요. 하나의 키로 두 인스턴스가 서명하는 것은 이중 서명이며 영구적인 슬래시로 이어집니다. ::: ### 2단계: 환경 설정 ```bash # Staking precompile contract address export STAKING_ADDRESS="0x0000000000000000000000000000000000000800" # Mainnet EVM RPC export RPC_URL="https://rpc.stable.xyz" # Your operator private key and validator EVM address export PRIVATE_KEY="your_private_key_here" export VALIDATOR_ADDRESS="0xYourValidatorAddress" # Consensus pubkey from Step 1 export PUBKEY="AbCd...base64PubKey...==" # Self-delegation amount in wei (18 decimals). 1000000000000000000 = 1 token export AMOUNT="1000000000000000000" ``` ### 3단계: 검증자 생성 스테이킹 프리컴파일에서 `createValidator`를 호출합니다. 이 함수는 `description` 튜플, `commissionRates` 튜플, 최소 자기 위임, 검증자 주소, 합의 공개 키, 그리고 본딩 금액을 받습니다. `cast`로 인코딩하여 전송하세요. ```bash # createValidator( # (moniker, identity, website, securityContact, details), # (rate, maxRate, maxChangeRate), # minSelfDelegation, validatorAddress, pubkey, value # ) cast send "$STAKING_ADDRESS" \ "createValidator((string,string,string,string,string),(uint256,uint256,uint256),uint256,address,string,uint256)" \ "(\"My Validator\",\"keybase-id\",\"https://example.com\",\"security@example.com\",\"My validator description\")" \ "(100000000000000000,200000000000000000,10000000000000000)" \ "1000000000000000000" \ "$VALIDATOR_ADDRESS" \ "$PUBKEY" \ "$AMOUNT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ```text transactionHash 0x4f...c2 status 1 (success) ``` 커미션 튜플은 `(rate, maxRate, maxChangeRate)`이며, 각각 18자리 소수로 스케일됩니다. 예제는 10% 비율(`100000000000000000`), 20% 상한선, 그리고 1% 최대 일일 변경률을 설정합니다. `maxRate`와 `maxChangeRate`는 생성 시 고정되며 나중에 수정할 수 없습니다. 성공적인 호출은 `CreateValidator` 이벤트를 발생시킵니다. 모든 필드에 대해서는 [스테이킹 프리컴파일 레퍼런스](/ko/reference/staking-module-api#createvalidator)를 참조하세요. ### 4단계: 검증 스테이킹 프리컴파일에서 다시 읽어 검증자가 등록되고 본딩되었는지 확인한 다음, 블록에 서명하고 있는지 확인하세요. ```bash # Read your validator's on-chain record cast call "$STAKING_ADDRESS" \ "validator(address)" "$VALIDATOR_ADDRESS" \ --rpc-url "$RPC_URL" # Confirm the node reports validator info curl -s localhost:26657/status | jq '.result.validator_info' ``` ```text # validator() returns the moniker, tokens, commission, and a bonded status (3) # validator_info shows your consensus address with non-zero voting power ``` ### 자기 위임 추가 생성 후 자신의 검증자에 더 많은 스테이크를 본딩하려면, 동일한 프리컴파일에서 `delegate`를 호출하세요. ```bash cast send "$STAKING_ADDRESS" \ "delegate(address,address,uint256)" \ "$VALIDATOR_ADDRESS" "$VALIDATOR_ADDRESS" "$AMOUNT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ```text status 1 (success) ``` ### 등록 후 검증자를 건강하게 유지하고 네트워크 업그레이드에 대비하세요: * [노드 모니터링하기](/ko/how-to/monitor-node)의 Prometheus 및 Grafana 스택으로 **서명과 누락된 블록을 모니터링**하세요. * 업그레이드 높이를 놓치지 않도록 **업그레이드를 자동화**하세요. [노드 설치하기](/ko/how-to/install-node#cosmovisor-setup-recommended-for-automatic-upgrades)와 [노드 업그레이드하기](/ko/how-to/upgrade-node)의 Cosmovisor 설정을 참조하세요. * [노드 문제 해결하기](/ko/how-to/troubleshoot-node)로 **문제를 진단**하세요(동기화되지 않음, 서명하지 않음). ### 다음 권장 사항 * [**스테이킹 프리컴파일 레퍼런스**](/ko/reference/staking-module-api) — 전체 createValidator, delegate, editValidator 시그니처와 구조체를 찾아보세요. * [**노드 구성**](/ko/reference/node-configuration) — 등록하기 전에 double\_sign\_check\_height 및 기타 검증자 핵심 구성을 설정하세요. * [**노드 모니터링하기**](/ko/how-to/monitor-node) — 서명, 누락된 블록, 리소스 사용량을 추적하여 슬래시 전에 문제를 포착하세요. * [**검증자 데이터 인덱싱하기**](/ko/how-to/index-validator-data) — 검증자가 활성화되면 온체인에서 스테이크, 가동 시간, 투표 이력을 읽으세요. ## viem과 함께 SDK 사용하기 `@stablechain/sdk`는 viem 기반으로 만들어졌습니다. `createStable`은 세 가지 서명 모드를 지원하며, 코드가 실행되는 위치에 따라 하나를 선택합니다: 프라이빗 키를 사용하는 서버 측, 사용자 지갑을 사용하는 브라우저 측, 또는 이미 구성한 `WalletClient`(예: wagmi 앱에서)를 사용하는 방식입니다. 이 가이드에서는 각 모드를 처음부터 끝까지 보여줍니다. ### 서버 측: 프라이빗 키 `Account` viem의 `privateKeyToAccount`를 사용해 백엔드가 보유한 프라이빗 키로 서명합니다. ```ts import "dotenv/config"; import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const stable = createStable({ network: Network.Mainnet, account, }); const { txHash } = await stable.transfer({ from: account.address, to: "0xRecipient", amount: 5, }); console.log(txHash); ``` ```text 0x8f3a...2d41 ``` ### 브라우저 측: 지갑에서 가져온 `Transport` `custom(window.ethereum)`(또는 모든 EIP-1193 프로바이더)을 `transport`로 전달합니다. SDK가 `WalletClient`를 구성하고 프로바이더에서 서명자 주소를 읽어옵니다. ```ts import { createStable, Network } from "@stablechain/sdk"; import { custom } from "viem"; const stable = createStable({ network: Network.Mainnet, transport: custom(window.ethereum), }); const [from] = await window.ethereum.request({ method: "eth_requestAccounts" }); const { txHash } = await stable.transfer({ from, to: "0xRecipient", amount: 5, }); ``` ```text 0x8f3a...2d41 ``` :::warning `transfer`, `bridge`, `swap`는 `switchChain`을 호출해 지갑을 올바른 네트워크로 전환합니다. 사용자가 거부하면 SDK는 `phase: "switch_chain"`과 함께 `StableTransactionError`를 발생시킵니다. 이를 잡아서 사용자에게 재시도를 표시하세요. ::: ### 직접 만든 `WalletClient` 사용하기 이미 `WalletClient`가 있는 경우(예: wagmi 또는 커스텀 서명자에서), 직접 전달하세요. 이는 `account` 및 `transport`보다 우선합니다. ```ts import { createStable, Network } from "@stablechain/sdk"; import { createWalletClient, custom } from "viem"; import { stable as stableChain } from "viem/chains"; const walletClient = createWalletClient({ chain: stableChain, transport: custom(window.ethereum), }); const [from] = await walletClient.requestAddresses(); const stable = createStable({ network: Network.Mainnet, walletClient, }); const { txHash } = await stable.transfer({ from, to: "0xRecipient", amount: 5 }); ``` ```text 0x8f3a...2d41 ``` ### 모드 선택하기 | **모드** | **사용 시점** | | :------------- | :----------------------------------------------------------- | | `account` | 백엔드 서비스, 스크립트, 에이전트 — 키를 보유한 모든 곳. | | `transport` | 사용자가 MetaMask 또는 wagmi 없는 커스텀 플로우로 서명하는 브라우저 앱. | | `walletClient` | 이미 구성된 `WalletClient`가 있는 경우(wagmi, RainbowKit, ConnectKit). | ### 다음 권장 사항 * [**wagmi와 함께 사용하기**](/ko/how-to/sdk-with-wagmi) — wagmi 훅을 통해 SDK를 React 앱에 연결합니다. * [**SDK 레퍼런스**](/ko/reference/sdk) — 모든 설정 필드, 메서드, 열거형, 에러 클래스. * [**SDK 빠른 시작**](/ko/tutorial/sdk-quickstart) — 테스트넷에서 첫 transfer, bridge, swap을 실행합니다. ## wagmi와 함께 SDK 사용하기 `createStable`은 viem `WalletClient`를 받는데, 이는 wagmi의 `useWalletClient`가 반환하는 것과 정확히 일치합니다. 평소처럼 wagmi를 통해 지갑을 연결한 다음, 지갑 클라이언트가 변경될 때마다 `StableClient`를 메모이즈합니다. 이 가이드는 wagmi v2와 `@tanstack/react-query`를 전제로 합니다. ### 1. wagmi 구성하기 wagmi 설정에 Stable을 추가합니다. viem은 두 네트워크 모두에 대한 체인 정의를 제공합니다. ```ts import { http, createConfig } from "wagmi"; import { stable as stableMainnet, stableTestnet } from "viem/chains"; import { injected } from "wagmi/connectors"; export const wagmiConfig = createConfig({ chains: [stableMainnet, stableTestnet], connectors: [injected()], transports: { [stableMainnet.id]: http(), [stableTestnet.id]: http(), }, }); ``` ```text WagmiConfig { chains: [988, 2201], connectors: [injected] } ``` ### 2. `StableClient`를 반환하는 훅 만들기 현재 `WalletClient`에 대해 `StableClient`를 메모이즈합니다. 지갑 클라이언트의 식별자가 변경되면 다시 생성합니다. ```tsx import { useMemo } from "react"; import { useWalletClient } from "wagmi"; import { createStable, Network, type StableClient } from "@stablechain/sdk"; export function useStable(network: Network = Network.Mainnet): StableClient | null { const { data: walletClient } = useWalletClient(); return useMemo(() => { if (!walletClient) return null; return createStable({ network, walletClient }); }, [walletClient, network]); } ``` :::warning `useWalletClient()`는 사용자가 연결하기 전에는 `undefined`를 반환합니다. SDK 메서드를 호출하기 전에 항상 가드를 두어야 합니다. 그렇지 않으면 구조 분해된 `walletClient`가 falsy가 되어 `createStable`이 서명자를 갖지 못합니다. ::: ### 3. 컴포넌트에서 사용하기 ```tsx import { useAccount, useChainId } from "wagmi"; import { Network } from "@stablechain/sdk"; import { useStable } from "./useStable"; export function PayButton() { const { address } = useAccount(); const chainId = useChainId(); const stable = useStable(Network.Mainnet); async function onClick() { if (!stable || !address) return; const { txHash } = await stable.transfer({ from: address, to: "0xRecipient", amount: 1, }); console.log("Sent:", txHash); } return ( ); } ``` ```text Sent: 0x8f3a...2d41 ``` ### 4. React에서 브리지와 스왑하기 동일한 `stable` 인스턴스가 브리지와 스왑을 처리합니다. effect나 `useQuery`에서 견적을 가져온 다음, 클릭 시 실행합니다. ```tsx const stable = useStable(Network.Mainnet); const onSwap = async () => { if (!stable) return; const quote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, }); const { txHash, toAmount } = await stable.swap({ fromToken: quote.fromToken, toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, quote, }); console.log({ txHash, toAmount }); }; ``` ```text { txHash: "0xabcd...", toAmount: 99.81 } ``` :::note `useQuery`로 견적을 캐싱하는 것이 잘 작동합니다. `quoteSwap` / `quoteBridge`를 쿼리 함수로 전달하고 캐시된 `quote`를 `swap` / `bridge`로 전달하세요. SDK는 견적이 제공되면 내부 견적 호출을 건너뜁니다. ::: ### 다음 권장 사항 * [**SDK 레퍼런스**](/ko/reference/sdk) — 모든 메서드, 설정 필드, 오류 클래스. * [**viem과 함께 사용하기**](/ko/how-to/sdk-with-viem) — 세 가지 서명 모드를 나란히 비교합니다. * [**SDK 빠른 시작**](/ko/tutorial/sdk-quickstart) — 테스트넷에서 첫 전송, 브리지, 스왑을 실행해 보세요. ## 자체 호스팅 가스 면제 자체 호스팅 가스 면제를 사용하면 호스팅된 Waiver Server API 대신 직접 면제 인프라를 운영할 수 있습니다. 온체인 거버넌스를 통해 면제 주소를 등록한 다음, 래퍼 트랜잭션을 네트워크에 직접 브로드캐스트합니다. 이 가이드에서는 면제 주소 등록, 서명된 사용자 트랜잭션 수집, 래퍼 트랜잭션 구성, 그리고 브로드캐스트 방법을 다룹니다. :::note **개념:** 가스 면제가 무엇이며 왜 존재하는지에 대해서는 [가스 면제](/ko/explanation/gas-waiver)를 참조하세요. 전체 프로토콜 사양(래퍼 트랜잭션 메커니즘, 권한 부여, 정책 확인, 실행 의미론, 보안 모델)에 대해서는 [가스 면제 프로토콜](/ko/reference/gas-waiver-api)을 참조하세요. ::: 호스팅된 Waiver Server API 통합 경로에 대해서는 [가스 비용 없는 트랜잭션 활성화](/ko/how-to/integrate-gas-waiver)를 참조하세요. ### 사전 요구사항 * 검증자 거버넌스를 통해 온체인에 등록된 면제 주소. * 대상 컨트랙트에 대해 구성된 `AllowedTarget` 정책. ### 개요 자체 호스팅 흐름: 1. `gasPrice = 0`인 **서명된 InnerTx를 사용자로부터 수집**합니다. 2. **WrapperTx를 구성**합니다: InnerTx를 RLP 인코딩하고 마커 주소로 전송되는 트랜잭션으로 래핑합니다. 3. `eth_sendRawTransaction`을 통해 WrapperTx를 **브로드캐스트**합니다. ### 1단계: 사용자의 InnerTx 수집 사용자는 `gasPrice = 0`으로 트랜잭션에 서명합니다. `to` 주소와 메서드 셀렉터는 면제의 `AllowedTarget` 정책과 일치해야 합니다. ```typescript // config.ts export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet MARKER_ADDRESS: "0x000000000000000000000000000000000000f333", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; ``` ```typescript // collectInnerTx.ts import { ethers } from "ethers"; import { CONFIG } from "./config"; const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ recipientAddress, ethers.parseUnits("0.01", 18) ]); const gasEstimate = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit: gasEstimate, nonce: nonce, chainId: CONFIG.CHAIN_ID, }; const signedInnerTx = await userWallet.signTransaction(innerTx); ``` ### 2단계: WrapperTx 구성 서명된 InnerTx를 RLP 인코딩하고 마커 주소로 전송되는 트랜잭션으로 래핑합니다. `gasLimit`은 내부 실행과 래핑 오버헤드를 모두 충당해야 합니다. ```typescript // constructWrapper.ts import { ethers } from "ethers"; import { CONFIG } from "./config"; const innerTxBytes = ethers.decodeRlp(signedInnerTx); const rlpEncoded = ethers.encodeRlp(innerTxBytes); const waiverNonce = await provider.getTransactionCount(waiverWallet.address); const wrapperTx = { to: CONFIG.MARKER_ADDRESS, data: rlpEncoded, value: 0, gasPrice: 0, gasLimit: (gasEstimate * 12n / 10n) * 2n, // ~2x inner gas for overhead nonce: waiverNonce, chainId: CONFIG.CHAIN_ID, }; const signedWrapperTx = await waiverWallet.signTransaction(wrapperTx); ``` :::warning `InnerTx.gasPrice`와 `WrapperTx.gasPrice` 모두 `0`이어야 합니다. `WrapperTx.value` 또한 `0`이어야 합니다. 이러한 조건 중 하나라도 충족되지 않으면 검증자가 트랜잭션을 거부합니다. ::: ### 3단계: 브로드캐스트 표준 JSON-RPC를 통해 서명된 WrapperTx를 제출합니다. ```typescript // broadcast.ts const txHash = await provider.send("eth_sendRawTransaction", [signedWrapperTx]); console.log("Wrapper tx broadcast:", txHash); const receipt = await provider.waitForTransaction(txHash); console.log("Confirmed:", receipt.status === 1); ``` ```text Wrapper tx broadcast: 0x... Confirmed: true ``` ### 핵심 요점 * 자체 호스팅 면제에는 온체인 검증자 거버넌스를 통해 등록된 면제 주소가 필요합니다. * WrapperTx는 RLP 인코딩된 InnerTx를 데이터로 하여 마커 주소(`0x...f333`)로 전송됩니다. * InnerTx와 WrapperTx 모두 `gasPrice = 0` 및 `value = 0`이어야 합니다. ### 다음 추천 * [**가스 면제 개념**](/ko/explanation/gas-waiver) — 직접 운영하기 전에 메커니즘을 이해하세요. * [**가스 면제 프로토콜**](/ko/reference/gas-waiver-api) — 마커 라우팅, 권한 부여, 실행 의미론에 대한 전체 프로토콜 사양을 참조하세요. * [**가스 비용 없는 트랜잭션 활성화**](/ko/how-to/integrate-gas-waiver) — 자체 호스팅 대신 호스팅된 Waiver Server API를 사용하세요. ## 구독 및 수금 이 가이드는 구독자가 한 번 인증하면 서비스 제공자가 EIP-7702 계정 추상화를 통해 각 청구 주기마다 자동으로 수금하는 구독 결제 시스템을 구축하는 과정을 안내합니다. :::note **개념:** 구독 모델, 트레이드오프, 카드 등록 청구와의 비교에 대해서는 [구독 청구](/ko/reference/subscriptions)를 참조하세요. ::: ### 무엇을 만들게 되나요 전체 구독 라이프사이클: 구독자가 한 번 위임 및 구독하고, 제공자가 일정에 따라 수금하며(반복 동작을 증명하기 위해 두 번째 주기 표시), 구독자가 취소합니다. #### 데모 ```text step 1. Subscriber delegates EOA to SubscriptionManager (EIP-7702) tx: 0x7702...aaaa step 2. Subscriber registers subscription (10 USDT0 / 30 days) subscriptionId: 0xabc... nextChargeAt: 2026-05-23T12:00:00Z step 3. Provider calls collect() on day 30 collected: 10 USDT0 gas cost: ~0.000050 USDT0 nextChargeAt: 2026-06-22T12:00:00Z step 4. Provider calls collect() on day 60 collected: 10 USDT0 gas cost: ~0.000050 USDT0 nextChargeAt: 2026-07-22T12:00:00Z step 5. Subscriber cancels subscription: inactive ``` ### 개요 **구독자:** ``` ─── Subscriber ─────────────────────────────────────── // One-time setup: delegate EOA to the subscription contract signAuthorization(delegateContract) sendTransaction({ type: 4, authorizationList: [signedAuth] }) // Subscribe: set billing terms on own EOA sendTransaction({ to: self, data: subscribe(subscriptionId, provider, amount, interval) }) // Cancel: revoke billing access at any time sendTransaction({ to: self, data: cancelSubscription(subscriptionId) }) ``` **서비스 제공자:** ``` ─── Service Provider ──────────────────────────────── // Each billing cycle: collect payment from subscriber's EOA // The delegate contract verifies caller, billing schedule, and amount sendTransaction({ to: subscriberEOA, data: collect(subscriptionId) }) // Automate with a cron job matching the billing interval // The contract reverts if called before the interval has elapsed ``` ### 위임 컨트랙트 구독 청구는 구독자의 EOA를 청구 조건을 강제하는 컨트랙트로 위임하여 작동합니다. EIP-7702를 통해 구독자의 계정은 일시적으로 컨트랙트 로직을 획득하므로, 서비스 제공자는 구독자가 매번 서명할 필요 없이 각 청구 주기마다 결제를 수금할 수 있습니다. 기존에 배포된 컨트랙트를 사용하거나 직접 배포할 수 있습니다. 아래 예시는 세 가지 작업을 지원하는 최소한의 `SubscriptionManager` 컨트랙트입니다: * `subscribe`: `subscriptionId`에 대한 청구 조건을 등록합니다. * `collect`: 제공자가 해당 `subscriptionId`에 대해 다음 예정된 결제를 수금합니다. * `cancelSubscription`: 구독자가 특정 구독을 취소합니다. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SubscriptionManager (example) /// @notice Delegate contract for EIP-7702 subscription billing. /// Runs on the subscriber's EOA via delegation. contract SubscriptionManager { struct Subscription { address provider; uint256 amount; uint256 interval; uint256 nextChargeAt; bool active; } // Keyed by subscriptionId. // Storage is already per subscriber EOA under delegation. mapping(bytes32 => Subscription) public subscriptions; IERC20 public immutable usdt0; event SubscriptionCreated( bytes32 indexed subscriptionId, address indexed provider, uint256 amount, uint256 interval, uint256 nextChargeAt ); event SubscriptionCollected( bytes32 indexed subscriptionId, address indexed provider, uint256 amount, uint256 collectedAt ); event SubscriptionCancelled(bytes32 indexed subscriptionId); constructor(address _usdt0) { usdt0 = IERC20(_usdt0); } /// @notice Register a subscription. Called by the subscriber on their own EOA. function subscribe( bytes32 subscriptionId, address provider, uint256 amount, uint256 interval ) external { require(msg.sender == address(this), "subscriber only"); require(provider != address(0), "invalid provider"); require(amount > 0, "invalid amount"); require(interval > 0, "invalid interval"); require(!subscriptions[subscriptionId].active, "already exists"); uint256 nextChargeAt = block.timestamp + interval; subscriptions[subscriptionId] = Subscription({ provider: provider, amount: amount, interval: interval, nextChargeAt: nextChargeAt, active: true }); emit SubscriptionCreated(subscriptionId, provider, amount, interval, nextChargeAt); } /// @notice Collect a payment for a specific subscription. Called by the service provider. function collect(bytes32 subscriptionId) external { Subscription storage sub = subscriptions[subscriptionId]; require(sub.active, "not active"); require(msg.sender == sub.provider, "not provider"); require(block.timestamp >= sub.nextChargeAt, "too early"); sub.nextChargeAt += sub.interval; require(usdt0.transfer(sub.provider, sub.amount), "transfer failed"); emit SubscriptionCollected(subscriptionId, sub.provider, sub.amount, block.timestamp); } /// @notice Cancel a specific subscription. Called by the subscriber. function cancelSubscription(bytes32 subscriptionId) external { require(msg.sender == address(this), "subscriber only"); require(subscriptions[subscriptionId].active, "not active"); delete subscriptions[subscriptionId]; emit SubscriptionCancelled(subscriptionId); } } ``` :::note 이 컨트랙트는 테스트 목적의 참조 구현으로 제공됩니다. 위임 컨트랙트는 구독자의 EOA에 대한 완전한 실행 권한을 가지므로, 프로덕션에서는 감사 및 검증된 컨트랙트를 사용하세요. EIP-7702 위임 및 보안에 대한 자세한 내용은 [EIP-7702](/ko/explanation/eip-7702)를 참조하세요. ::: ### 구성 ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const SUBSCRIPTION_MANAGER = "0xYourDeployedSubscriptionManager"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const subscriberWallet = new ethers.Wallet(process.env.SUBSCRIBER_KEY!, provider); ``` ### 1단계: 구독자의 EOA 위임 (EIP-7702) 구독자는 EIP-7702 인증에 서명하여 자신의 EOA를 `SubscriptionManager`로 위임합니다. 이후 구독자의 EOA는 위임 컨트랙트의 로직을 실행합니다. ```typescript // delegate.ts import { subscriberWallet, provider, CHAIN_ID, SUBSCRIPTION_MANAGER } from "./config"; const authorization = { chainId: CHAIN_ID, address: SUBSCRIPTION_MANAGER, nonce: await provider.getTransactionCount(subscriberWallet.address), }; const signedAuth = await subscriberWallet.signAuthorization(authorization); const tx = await subscriberWallet.sendTransaction({ type: 4, to: subscriberWallet.address, authorizationList: [signedAuth], maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Delegation tx:", receipt.hash); ``` ```bash npx tsx delegate.ts ``` ```text Delegation tx: 0x7702...aaaa ``` ### 2단계: 구독 등록 (구독자) 구독자는 자신의 EOA에서 `subscribe()`를 호출합니다. EOA가 위임되어 있으므로 이는 `SubscriptionManager.subscribe`를 실행합니다. ```typescript // subscribe.ts import { ethers } from "ethers"; import { subscriberWallet } from "./config"; const subscriptionManager = new ethers.Interface([ "function subscribe(bytes32 subscriptionId, address provider, uint256 amount, uint256 interval)", ]); const serviceProvider = "0xServiceProviderAddress"; const monthlyAmount = ethers.parseUnits("10", 6); // 10 USDT0 const interval = 30 * 24 * 60 * 60; // 30 days in seconds // Derive a unique subscriptionId from provider + plan name + local nonce const subscriptionId = ethers.solidityPackedKeccak256( ["address", "string", "uint256"], [serviceProvider, "pro-monthly", 1] ); const tx = await subscriberWallet.sendTransaction({ to: subscriberWallet.address, // call self (delegate code executes) data: subscriptionManager.encodeFunctionData("subscribe", [ subscriptionId, serviceProvider, monthlyAmount, interval, ]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Subscription registered, tx:", receipt.hash); console.log("Subscription ID:", subscriptionId); ``` ```bash npx tsx subscribe.ts ``` ```text Subscription registered, tx: 0xabcd...1234 Subscription ID: 0xfedc...9876 ``` ### 3단계: 결제 수금 (서비스 제공자) 각 청구 주기마다 서비스 제공자는 구독자의 EOA에서 `collect(subscriptionId)`를 호출합니다. 위임 로직은 USDT0를 전송하기 전에 호출자, 청구 일정, 금액을 검증합니다. ```typescript // collect.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const providerWallet = new ethers.Wallet(process.env.PROVIDER_KEY!, provider); const subscriptionManager = new ethers.Interface([ "function collect(bytes32 subscriptionId)", ]); const subscriberEOA = "0xSubscriberEOAAddress"; const subscriptionId = "0xYourSubscriptionId"; const tx = await providerWallet.sendTransaction({ to: subscriberEOA, // subscriber's EOA (runs delegate code) data: subscriptionManager.encodeFunctionData("collect", [subscriptionId]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Payment collected, tx:", receipt.hash); console.log("Gas used:", receipt.gasUsed.toString()); // In production, run this on a cron schedule matching the billing interval. // The delegate contract will revert if called before the interval has elapsed. ``` ```bash npx tsx collect.ts ``` ```text Payment collected, tx: 0x8f3a...2d41 Gas used: 52000 ``` `collect()` 호출은 Stable에서 대략 **50k-55k 가스**(21k 기본 + 7702 위임 오버헤드 + ERC-20 `transfer`)가 소요됩니다. 1 gwei 기본 수수료 기준으로, 이는 제공자가 청구 주기당 약 `0.000050 USDT0`를 지불하는 것입니다. ### 4단계: 구독 취소 (구독자) 구독자는 자신의 EOA에서 `cancelSubscription(subscriptionId)`를 호출하여 해당 특정 구독에 대한 청구 접근 권한을 취소합니다. ```typescript // cancel.ts import { ethers } from "ethers"; import { subscriberWallet } from "./config"; const subscriptionManager = new ethers.Interface([ "function cancelSubscription(bytes32 subscriptionId)", ]); const subscriptionId = "0xYourSubscriptionId"; const tx = await subscriberWallet.sendTransaction({ to: subscriberWallet.address, data: subscriptionManager.encodeFunctionData("cancelSubscription", [subscriptionId]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Subscription cancelled, tx:", receipt.hash); ``` ```bash npx tsx cancel.ts ``` ```text Subscription cancelled, tx: 0xdef0...5678 ``` ### 보안 모델 구독자는 위임 컨트랙트가 자신의 EOA에서 자금을 인출하도록 인증하는 것입니다. 그 인증이 정확히 무엇을 포함하는지, 그리고 노출을 어떻게 제한할지 이해하세요. **구독자가 인증하는 것.** `SubscriptionManager`에 위임함으로써 구독자는 컨트랙트의 로직에 자신의 EOA에 대한 완전한 실행 권한을 부여합니다. 위임은 컨트랙트에 코딩된 조건 하에서만 자금을 전송할 수 있습니다: 호출자가 등록된 제공자이고, 간격이 경과했으며, 금액이 저장된 구독과 일치하는 경우입니다. 컨트랙트 코드가 그러한 동작을 허용하지 않기 때문에, 다른 주소로 전송하거나 간격 검사를 우회할 수 없습니다. **완화해야 할 실패 모드.** * **악의적인 위임 업그레이드**: `SubscriptionManager`가 관리자에 의해 구현이 변경될 수 있는 프록시인 경우, 인증은 사실상 해당 관리자를 신뢰하는 것입니다. 불변 컨트랙트 또는 투명하고 타임락이 적용된 업그레이드를 가진 프록시에만 위임하세요. * **제공자 침해**: 제공자의 키가 유출되면, 공격자는 주기당 금액까지 조기 결제를 수금할 수 있습니다. 구독자는 구독별로 `spendingLimit`을 설정하고 인증되지 않은 `SubscriptionCollected` 이벤트를 모니터링해야 합니다. * **위임 교체**: 다른 위임으로 다시 구독하면 구독 상태가 지워집니다. 기능별로 하나의 위임을 사용하는 대신, 단일 위임 하에서 여러 함수(구독, 일괄 결제, 지출 한도)를 지원하는 모듈식 위임을 사용하세요. * **재생 가능한 서명**: 모든 서명은 구독자의 EOA에 연결된 EIP-7702 nonce를 사용하므로, 체인 간 또는 위임 간에 재생할 수 없습니다. **권장 가드레일.** * 프로덕션 사용 전에 위임 컨트랙트를 감사하세요. * 구독별 금액을 구독자의 잔액 대비 작게 유지하세요. * `SubscriptionCreated` / `SubscriptionCollected` 이벤트를 모니터링하고 구독자에게 표시하세요. * 구독자에게 자신의 EOA에서 `cancelSubscription(subscriptionId)`을 호출하는 명확한 "취소" UI를 제공하세요. ### 중요한 고려 사항 * **지속적인 위임**: EIP-7702 위임은 구독자가 명시적으로 변경하거나 지울 때까지 지속됩니다. 각 청구 주기마다 재위임이 필요하지 않습니다. * **EOA당 단일 위임**: 구독자가 나중에 다른 컨트랙트로 위임하면, 구독 위임 로직이 교체되어 수금이 실패합니다. 단일 위임 하에서 여러 함수(구독, 일괄 결제, 지출 한도, 세션 키)를 지원하는 모듈식 위임 컨트랙트를 사용하세요. * **일정 동작**: 이 예시는 각 성공적인 수금마다 `nextChargeAt`을 한 간격씩 진행합니다. 둘 이상의 청구 기간이 경과한 경우, 반복적인 `collect()` 호출로 한 번에 한 기간씩 따라잡을 수 있습니다. 제품에 다른 정책이 필요한 경우 로직을 확장하세요. * **감사된 위임 사용**: 감사를 받은 컨트랙트에만 위임하세요. ### 다음 권장 사항 * [**구독 청구 개념**](/ko/reference/subscriptions) — 풀 기반 청구 모델을 이해하세요. * [**계정 추상화**](/ko/how-to/account-abstraction) — 일괄 결제, 지출 한도, 세션 키가 하나의 위임 하에서 어떻게 결합되는지 확인하세요. * [**EIP-7702 개념**](/ko/explanation/eip-7702) — 이를 가능하게 하는 위임 모델을 검토하세요. ## 언본딩 완료 추적하기 언본딩 기간이 완료되면, 프로토콜은 시스템 트랜잭션을 통해 `StableSystem` 프리컴파일(`0x0000000000000000000000000000000000009999`)에서 `UnbondingCompleted` 이벤트를 발생시킵니다. 이를 통해 dApp은 커스텀 인덱서를 실행하거나 REST 엔드포인트를 폴링하지 않고도 사용자에게 실시간으로 알리고 잔액을 업데이트할 수 있습니다. :::note **개념:** 시스템 트랜잭션이 SDK 계층의 이벤트를 EVM으로 연결하는 방식과 그 중요성에 대해서는 [시스템 트랜잭션](/ko/explanation/system-transactions)을 참고하세요. ::: ### 사전 요구 사항 * [시스템 트랜잭션](/ko/explanation/system-transactions)에 대한 이해. * [스테이킹](/ko/explanation/staking-module), 특히 `undelegate`와 언본딩 프로세스에 대한 이해. * 표준 web3 라이브러리(예: [ethers.js](https://docs.ethers.org/) v6)를 사용한 컨트랙트 이벤트 구독 및 필터링 경험. ### 개요 * **컨트랙트 인스턴스 설정**: StableSystem 프리컴파일을 위한 컨트랙트 인스턴스를 생성합니다. * **애플리케이션에서 이벤트 처리**: 애플리케이션 로직에 따라 실시간 이벤트를 구독하거나 과거 데이터를 조회합니다. * **연결 문제 처리**: 지속적인 WebSocket 구독을 위한 재연결 로직을 구현합니다. ### 1단계: 컨트랙트 인스턴스 설정 `UnbondingCompleted` 이벤트 ABI를 사용하여 `StableSystem` 프리컴파일을 위한 컨트랙트 인스턴스를 생성합니다. ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_SYSTEM_ADDRESS = "0x0000000000000000000000000000000000009999"; export const STABLE_SYSTEM_ABI = [ "event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)", ]; export const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); export const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); ``` ### 2단계: 애플리케이션에서 이벤트 처리 애플리케이션 로직에 따라 실시간 이벤트를 구독하거나, 과거 데이터를 조회하거나, 둘 다 수행합니다. #### 실시간 구독 언본딩이 완료될 때 실시간 알림을 받기 위해 `UnbondingCompleted` 이벤트를 구독합니다. 잔액 업데이트 트리거, 알림 전송, 또는 대시보드 통계 새로고침에 유용합니다. ```typescript // subscribeBasic.ts import { stableSystem } from "./config"; stableSystem.on("UnbondingCompleted", (delegator, validator, amount, event) => { console.log("Unbonding completed:"); console.log(" Delegator:", delegator); console.log(" Validator:", validator); console.log(" Amount:", ethers.formatEther(amount), "tokens"); console.log(" Block:", event.log.blockNumber); console.log(" Tx Hash:", event.log.transactionHash); }); ``` #### 사용자별 필터링 특정 위임자 주소에 대한 이벤트만 수신하려면, 인덱싱된 이벤트 매개변수를 사용하여 필터를 생성합니다. ```typescript // subscribeByUser.ts import { ethers } from "ethers"; import { stableSystem } from "./config"; const userAddress = "0xabcd..."; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { refreshUserBalance(userAddress); showNotification( `Your unbonding of ${ethers.formatEther(amount)} tokens completed!` ); }); ``` #### 검증자별 필터링 ```typescript // subscribeByValidator.ts import { stableSystem } from "./config"; const validatorAddress = "0x1234..."; const validatorFilter = stableSystem.filters.UnbondingCompleted( null, validatorAddress ); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### 과거 데이터 조회 dApp이 과거의 언본딩 완료 내역을 표시해야 하는 경우, 블록 범위와 함께 이벤트 필터를 사용하여 과거 이벤트를 조회합니다. ```typescript // queryHistory.ts import { ethers } from "ethers"; import { provider, stableSystem } from "./config"; async function getUnbondingHistory( userAddress: string, fromBlock: number, toBlock: number ) { const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter(filter, fromBlock, toBlock); return events.map((event) => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash, })); } const currentBlock = await provider.getBlockNumber(); const history = await getUnbondingHistory( "0xabcd...", currentBlock - 1000, currentBlock ); ``` ### 3단계: 연결 문제 처리 이벤트 구독은 지속적인 WebSocket 연결에 의존합니다. 프로덕션 dApp을 위해 재연결 로직을 구현하세요. ```typescript // subscribeWithReconnection.ts import { ethers } from "ethers"; import { STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI } from "./config"; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function handleUnbonding(delegator: string, validator: string, amount: bigint) { console.log("Unbonding completed:", { delegator, validator, amount }); } function setupEventListener() { const wsProvider = new ethers.WebSocketProvider("wss://rpc.testnet.stable.xyz"); wsProvider.on("error", (error) => { console.error("Provider error:", error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, wsProvider ); stableSystem.on("UnbondingCompleted", handleUnbonding); } setupEventListener(); ``` ### 다음 추천 항목 * [**시스템 트랜잭션 개념**](/ko/explanation/system-transactions) — 프로토콜 수준의 이벤트가 EVM에 도달하는 방식을 이해합니다. * [**스테이킹 모듈 개념**](/ko/explanation/staking-module) — 위임 및 언본딩 흐름을 검토합니다. * [**스테이킹 프리컴파일 레퍼런스**](/ko/reference/staking-module-api) — 여기서 추적하는 이벤트를 트리거하는 메서드를 찾아봅니다. 이 종합 가이드는 Stable 노드의 일반적인 문제를 진단하고 해결하는 데 도움을 줍니다. ### 빠른 진단 #### 노드 상태 점검 스크립트 ```bash #!/bin/bash # quick-diagnosis.sh # Set service name (default: stable) export SERVICE_NAME=stable echo "=== Stable Node Diagnostics ===" echo "Timestamp: $(date)" echo "" # 1. Service Status echo "1. SERVICE STATUS:" systemctl status ${SERVICE_NAME} --no-pager | head -10 # 2. Sync Status echo -e "\n2. SYNC STATUS:" curl -s localhost:26657/status | jq '.result.sync_info' 2>/dev/null || echo "RPC not responding" # 3. Peer Connections echo -e "\n3. PEER COUNT:" curl -s localhost:26657/net_info | jq '.result.n_peers' 2>/dev/null || echo "Cannot get peer info" # 4. Recent Errors echo -e "\n4. RECENT ERRORS (last 20):" sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -i error | tail -20 # 5. System Resources echo -e "\n5. SYSTEM RESOURCES:" df -h / | grep -v Filesystem free -h | grep Mem top -bn1 | grep "load average" # 6. Port Status echo -e "\n6. PORT STATUS:" ss -tulpn | grep ${SERVICE_NAME} || echo "No ${SERVICE_NAME} ports found" echo -e "\n=== Diagnostics Complete ===" ``` ### 일반적인 문제와 해결책 #### 노드가 시작되지 않음 ##### 문제: 바이너리를 찾을 수 없음 **오류 메시지:** ``` stabled: command not found ``` **해결책:** ```bash # Check if binary exists ls -la /usr/bin/stabled # If missing, reinstall (use arm64 if needed) wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz tar -xvzf stabled-0.7.2-linux-amd64-testnet.tar.gz sudo mv stabled /usr/bin/ sudo chmod +x /usr/bin/stabled ``` ##### 문제: 권한 거부됨 **오류 메시지:** ``` Error: open /home/user/.stabled/config/config.toml: permission denied ``` **해결책:** ```bash # Fix ownership sudo chown -R $USER:$USER ~/.stabled/ # Fix permissions chmod 700 ~/.stabled/ chmod 600 ~/.stabled/config/*.json chmod 644 ~/.stabled/config/*.toml ``` ##### 문제: 주소가 이미 사용 중 **오류 메시지:** ``` Error: listen tcp 0.0.0.0:26657: bind: address already in use ``` **해결책:** ```bash # Find process using port sudo lsof -i :26657 # Kill the process sudo kill -9 # Or change port in config sed -i 's/laddr = "tcp:\/\/0.0.0.0:26657"/laddr = "tcp:\/\/0.0.0.0:26658"/' ~/.stabled/config/config.toml ``` #### 동기화 문제 ##### 문제: 노드가 특정 높이에서 멈춤 **증상:** * 블록 높이가 증가하지 않음 * 1분 이상 새 블록이 없음 **해결책:** ```bash # 1. Check peers curl localhost:26657/net_info | jq '.result.n_peers' # If no peers, add persistent peers echo "persistent_peers = \"5ed0f977a26ccf290e184e364fb04e268ef16430@37.187.147.27:26656,128accd3e8ee379bfdf54560c21345451c7048c7@37.187.147.22:26656\"" >> ~/.stabled/config/config.toml # 2. Reset and resync sudo systemctl stop ${SERVICE_NAME} stabled comet unsafe-reset-all --keep-addr-book sudo systemctl start ${SERVICE_NAME} # 3. Use snapshot (see Snapshots guide) ``` ##### 문제: "wrong Block.Header.AppHash" 오류 **오류 메시지:** ``` panic: Wrong Block.Header.AppHash. Expected XXXX, got YYYY ``` **해결책:** ```bash # This indicates state corruption - rollback to previous block sudo systemctl stop ${SERVICE_NAME} # Rollback one block stabled rollback # Restart node sudo systemctl start ${SERVICE_NAME} # If rollback doesn't work, restore from snapshot # Backup important files cp ~/.stabled/config/priv_validator_key.json ~/backup/ cp ~/.stabled/config/node_key.json ~/backup/ # Reset state stabled comet unsafe-reset-all # Restore from snapshot wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 tar -I lz4 -xf snapshot.tar.lz4 -C ~/.stabled/ sudo systemctl start ${SERVICE_NAME} ``` ##### 문제: 느린 동기화 속도 **증상:** * 분당 100블록 미만 * 높은 CPU/디스크 사용량 **해결책:** ```bash # 1. Check disk I/O iostat -x 1 5 # 2. Optimize configuration cat >> ~/.stabled/config/config.toml <> ~/.stabled/config/config.toml < db_dump.txt # 4. If repair fails, resync rm -rf ~/.stabled/data # Restore from snapshot # 5. Start node sudo systemctl start ${SERVICE_NAME} ``` ##### 문제: "too many open files" **오류 메시지:** ``` accept: too many open files ``` **해결책:** ```bash # 1. Check current limits ulimit -n # 2. Increase limits echo "* soft nofile 65535" | sudo tee -a /etc/security/limits.conf echo "* hard nofile 65535" | sudo tee -a /etc/security/limits.conf # 3. Update systemd service sudo sed -i '/\[Service\]/a LimitNOFILE=65535' /etc/systemd/system/stabled.service # 4. Reload and restart sudo systemctl daemon-reload sudo systemctl restart ${SERVICE_NAME} ``` #### 메모리 문제 ##### 문제: 메모리 부족(OOM) 종료 **증상:** ``` stabled.service: Main process exited, code=killed, status=9/KILL ``` **해결책:** ```bash # 1. Check memory usage free -h dmesg | grep -i "killed process" # 2. Add swap space sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 3. Optimize memory usage cat >> ~/.stabled/config/app.toml < $OUTPUT_DIR/system.txt df -h >> $OUTPUT_DIR/system.txt free -h >> $OUTPUT_DIR/system.txt # Service status systemctl status ${SERVICE_NAME} --no-pager > $OUTPUT_DIR/service-status.txt # Recent logs sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" > $OUTPUT_DIR/recent-logs.txt # Config files (remove sensitive data) grep -v "priv" ~/.stabled/config/config.toml > $OUTPUT_DIR/config.toml grep -v "priv" ~/.stabled/config/app.toml > $OUTPUT_DIR/app.toml # Node status curl -s localhost:26657/status > $OUTPUT_DIR/node-status.json 2>/dev/null # Create archive tar -czf $OUTPUT_DIR.tar.gz $OUTPUT_DIR/ echo "Debug info collected: $OUTPUT_DIR.tar.gz" echo "Share this file when requesting support" ``` ### 다음 단계 * 문제를 예방하려면 [모니터링 설정](/ko/how-to/monitor-node)을 검토하세요 * 버전별 문제는 [업그레이드 가이드](/ko/how-to/upgrade-node)를 확인하세요 이 가이드는 업그레이드 절차와 롤백 전략을 포함하여 Stable 노드의 업그레이드 과정을 다룹니다. > 전체 버전 기록 및 업그레이드 세부 사항은 [버전 기록](/ko/reference/testnet-version-history)을 참조하세요. ### 업그레이드 유형 #### 소프트 업그레이드 (비호환성 변경 없음) * 언제든지 수행 가능 * 하위 호환성 유지 #### 하드 업그레이드 (호환성 변경) * 특정 높이에서 업그레이드 필요 * 하위 호환되지 않음 #### 긴급 업그레이드 * 중요한 보안 수정 * 즉각적인 조치 필요 * 체인 중단이 필요할 수 있음 ### 표준 업그레이드 절차 #### 1단계: 준비 ```bash # Check current version stabled version --long # Backup critical data cp -r ~/.stabled/config ~/stable-backup-$(date +%Y%m%d)/ # For validators only: Backup validator state cp ~/.stabled/data/priv_validator_state.json ~/stable-backup-$(date +%Y%m%d)/ # Check disk space (need 2x current data size) df -h ~/.stabled ``` #### 2단계: 새 바이너리 다운로드 ```bash # For v1.2.0-rc1 upgrade (January 22, 2026) # Choose your architecture: # Linux AMD64 BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz" # OR Linux ARM64 BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz" # Download new binary wget $BINARY_URL # Extract to temporary location tar -xvzf stabled-1.2.0-rc1-linux-*.tar.gz -C /tmp/ # Verify new version /tmp/stabled version --long ``` #### 3단계: 업그레이드 수행 ##### 소프트 업그레이드의 경우 ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Backup current binary sudo mv /usr/bin/stabled /usr/bin/stabled.backup # Install new binary sudo mv /tmp/stabled /usr/bin/stabled sudo chmod +x /usr/bin/stabled # Verify installation stabled version --long # Start node sudo systemctl start ${SERVICE_NAME} # Monitor logs sudo journalctl -u ${SERVICE_NAME} -f ``` ##### 하드 업그레이드의 경우 ```bash # Monitor for upgrade height while true; do HEIGHT=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current height: $HEIGHT" if [ $HEIGHT -ge $UPGRADE_HEIGHT ]; then break fi sleep 10 done # Node will halt automatically at upgrade height # Wait for halt message in logs sudo journalctl -u ${SERVICE_NAME} -f | grep "UPGRADE" # Once halted, perform upgrade sudo systemctl stop ${SERVICE_NAME} sudo mv /usr/bin/stabled /usr/bin/stabled.backup sudo mv /tmp/stabled /usr/bin/stabled # Start with new binary sudo systemctl start ${SERVICE_NAME} ``` #### 4단계: 업그레이드 후 검증 ```bash # Check node status curl -s localhost:26657/status | jq '.result' # Verify version curl -s localhost:26657/status | jq '.result.node_info.version' # Check peers curl -s localhost:26657/net_info | jq '.result.n_peers' # Monitor sync status watch -n 2 'curl -s localhost:26657/status | jq ".result.sync_info"' # Check for errors sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep -i error ``` ### Cosmovisor 설정 (자동 업그레이드) Cosmovisor는 조율된 업그레이드를 위한 업그레이드 과정을 자동화합니다. #### 설치 ```bash # Install cosmovisor go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest # Or download binary wget https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.0/cosmovisor-v1.7.0-linux-amd64.tar.gz tar -xzf cosmovisor-v1.7.0-linux-amd64.tar.gz sudo mv cosmovisor /usr/bin/ ``` #### 구성 ```bash # Set environment variables cat >> ~/.bashrc < /dev/null < > export.json # 3. Wait for coordinated restart instructions ``` ### 다음 단계 * [버전 기록](/ko/reference/testnet-version-history) - 전체 업그레이드 기록 및 릴리스 노트 * 업그레이드 후 [노드 모니터링](/ko/how-to/monitor-node) * 일반적인 문제는 [문제 해결](/ko/how-to/troubleshoot-node)을 검토하세요 ## Stable 테스트넷 지갑에 자금을 충전하는 방법 Stable은 USDT0를 가스 토큰으로 사용하므로, 체인과 상호작용하기 위해서는 지갑에 USDT0가 필요합니다. 먼저 Faucet을 사용하여 계정에 USDT0를 충전해야 합니다. 1. [https://faucet.stable.xyz](https://faucet.stable.xyz) 를 방문합니다 2. 'Get USDT0' 버튼을 클릭하면 1 USDT0가 지갑으로 전송됩니다. 더 많은 USDT0가 필요한 경우, Ethereum Sepolia에서 Stable 테스트넷으로 Test USDT를 브리지할 수 있습니다. 1. [https://sepolia.etherscan.io/token/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract](https://sepolia.etherscan.io/token/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) 를 방문하여, `_mint` 함수를 호출해 원하는 양의 Test Tether USD를 계정에 발행합니다. 2. Ethereum Sepolia의 LayerZero 브리지 컨트랙트로 다음 트랜잭션을 전송하여 Test USDT를 Stable 테스트넷으로 브리지합니다: ```jsx export function addrTo32Bytes(addr: string): Buffer { const hex20 = ethers.utils.getAddress(addr).slice(2); const padded = hex20.padStart(64, "0"); // 32 bytes ⇒ 64 hex return Buffer.from(padded, "hex"); // length === 32 } async function main() { const [owner] = await ethers.getSigners(); const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927"; const SEPOLIA_USDT0_OAPP = "0xc099cD946d5efCC35A99D64E808c1430cEf08126" const RECEIVER_EID = 40374; const usdt0 = await ethers.getContractAt("ERC20", SEPOLIA_USDT0); await usdt0.approve(SEPOLIA_USDT0_OAPP, ethers.utils.parseEther("1")); const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(); const amount = ethers.utils.parseEther("1"); // Change this to your desired amount const OFTAdapter = await ethers.getContractAt("OFTAdapter", SEPOLIA_USDT0_OAPP); const sendParams = { dstEid: RECEIVER_EID, to: addrTo32Bytes(owner.address), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: Buffer.from(""), oftCmd: Buffer.from(""), }; const fee = await OFTAdapter.quoteSend(sendParams, false); await OFTAdapter.send( sendParams, fee, owner.address, { value: fee.nativeFee, } ) } ``` 이 가이드는 스냅샷과 상태 동기화를 사용하여 Stable 노드를 빠르게 동기화하는 다양한 방법을 다룹니다. ### 동기화 방법 개요 | 방법 | 동기화 시간 | 필요 저장 공간 | 사용 사례 | | -------------- | ------ | -------- | ----------------- | | **Pruned 스냅샷** | \~10분 | \< 5 GiB | 일반 풀 노드 | | **아카이브 스냅샷** | \~1시간 | \~500 GB | 아카이브 노드, 블록 익스플로러 | ### 공식 스냅샷 Stable은 매일(00:00 UTC) 업데이트되는 공식 스냅샷을 제공합니다. #### 스냅샷 정보 #### 메인넷 | 유형 | 압축 | 크기 | URL | 업데이트 주기 | | ---------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- | ------- | | **Pruned** | LZ4 | \< 5 GiB | [다운로드](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4) | 매일 | | **아카이브** | ZSTD | \~300 GB | [다운로드](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst) | 매주 | #### 테스트넷 | 유형 | 압축 | 크기 | URL | 업데이트 주기 | | ---------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- | ------- | | **Pruned** | LZ4 | \< 5 GiB | [다운로드](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4) | 매일 | | **아카이브** | ZSTD | \~800 GB | [다운로드](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst) | 매주 | ### Pruned 스냅샷 사용 Pruned 스냅샷은 최근 블록체인 상태(마지막 100\~1000개 블록)를 포함합니다. #### 1단계: 환경 변수 설정 ```bash # Set service name (default: stable) export SERVICE_NAME=stable ``` #### 2단계: 노드 서비스 중지 ```bash # Stop the running node sudo systemctl stop ${SERVICE_NAME} # Verify it's stopped sudo systemctl status ${SERVICE_NAME} ``` #### 3단계: 현재 데이터 백업(선택 사항) ```bash # Create backup directory mkdir -p ~/stable-backup # Backup current state (optional, requires significant space) cp -r ~/.stabled/data ~/stable-backup/ ``` #### 4단계: Pruned 스냅샷 다운로드 및 추출 :::code-group ```bash [Mainnet] # Install dependencies sudo apt install -y wget zstd pv # Create snapshot directory mkdir -p ~/snapshot cd ~/snapshot # Download pruned snapshot with progress wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4 # Remove old data rm -rf ~/.stabled/data/* # Extract snapshot with progress indicator pv stable_pruned.tar.zst | zstd -d -c | tar -xf - -C ~/.stabled/ # Alternative extraction without pv zstd -d stable_pruned.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm stable_pruned.tar.zst ``` ```bash [Testnet] # Install dependencies sudo apt install -y wget lz4 pv # Create snapshot directory mkdir -p ~/snapshot cd ~/snapshot # Download pruned snapshot with progress wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 # Alternative: Download with resume support curl -C - -o snapshot.tar.lz4 https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 # Remove old data rm -rf ~/.stabled/data/* # Extract snapshot with progress indicator pv snapshot.tar.lz4 | tar -I lz4 -xf - -C ~/.stabled/ # Alternative extraction without pv tar -I lz4 -xvf snapshot.tar.lz4 -C ~/.stabled/ # Clean up rm snapshot.tar.lz4 ``` ::: #### 5단계: 노드 재시작 ```bash # Start the node sudo systemctl start ${SERVICE_NAME} # Check status sudo systemctl status ${SERVICE_NAME} # Monitor logs sudo journalctl -u stabled -f ``` ### 아카이브 스냅샷 사용 아카이브 스냅샷은 전체 블록체인 기록을 포함합니다. #### 1단계: 시스템 준비 ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Install dependencies sudo apt install -y wget zstd pv # Check available disk space (need 2x snapshot size) df -h ~/.stabled ``` #### 2단계: 아카이브 스냅샷 다운로드 및 추출 :::code-group ```bash [Mainnet] # Create working directory mkdir -p ~/snapshot cd ~/snapshot # Download archive snapshot wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst # Clear old data rm -rf ~/.stabled/data/* # Extract with high memory for better performance pv stable_archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/ # Alternative: Standard extraction zstd -d --long=31 stable_archive.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm stable_archive.tar.zst ``` ```bash [Testnet] # Create working directory mkdir -p ~/snapshot cd ~/snapshot # Download archive snapshot wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst # Clear old data rm -rf ~/.stabled/data/* # Extract with high memory for better performance pv archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/ # Alternative: Standard extraction zstd -d --long=31 archive.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm archive.tar.zst ``` ::: #### 3단계: 노드 시작 ```bash # Start service sudo systemctl start ${SERVICE_NAME} # Verify sync status curl -s localhost:26657/status | jq '.result.sync_info' ``` ### 직접 스냅샷 만들기 #### 수동 스냅샷 생성 ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Create snapshot archive cd ~/.stabled tar -cf - data/ | lz4 -9 > ~/stable-snapshot-$(date +%Y%m%d).tar.lz4 # Create checksum sha256sum ~/stable-snapshot-*.tar.lz4 > checksums.txt # Restart node sudo systemctl start ${SERVICE_NAME} ``` #### 자동 스냅샷 스크립트 ```bash #!/bin/bash # snapshot.sh - Automated snapshot creation # Configuration SNAPSHOT_DIR="/var/snapshots" STABLED_HOME="$HOME/.stabled" KEEP_DAYS=7 # Create snapshot directory mkdir -p $SNAPSHOT_DIR # Stop node sudo systemctl stop ${SERVICE_NAME} # Create snapshot SNAPSHOT_NAME="stable-snapshot-$(date +%Y%m%d-%H%M%S).tar.lz4" tar -cf - -C $STABLED_HOME data/ | lz4 -9 > $SNAPSHOT_DIR/$SNAPSHOT_NAME # Generate metadata cat > $SNAPSHOT_DIR/latest.json < { console.log("Unbonding completed for:", delegator); console.log("Amount:", ethers.formatEther(amount), "STABLE"); console.log("Tx:", event.log.transactionHash); }); console.log("Listening for UnbondingCompleted events..."); ``` ```bash npx tsx watchUnbonding.ts ``` ```text Listening for UnbondingCompleted events... Unbonding completed for: 0xabcd... Amount: 100.0 STABLE Tx: 0x12ab... ``` 시스템 트랜잭션 메커니즘 전체와 사용자별 필터링 / 과거 조회 패턴은 [위임 해제 완료 추적하기](/ko/how-to/track-unbonding)를 참조하세요. ### 모듈별 레퍼런스 각 프리컴파일의 전체 메서드 목록, 이벤트, 권한 부여 규칙은 해당 레퍼런스 페이지에 있습니다. * [Bank 프리컴파일](/ko/reference/bank-module-api): STABLE 토큰 전송 및 공급량 조회. * [Distribution 프리컴파일](/ko/reference/distribution-module-api): 보상 청구 및 커미션. * [Staking 프리컴파일](/ko/reference/staking-module-api): 위임, 위임 해제, 재위임, 검증자 조회. * [시스템 트랜잭션](/ko/reference/system-transactions-api): StableSystem 이벤트 형식 및 권한 부여. ### 다음 추천 * [**위임 해제 완료 추적하기**](/ko/how-to/track-unbonding) — StableSystem 프리컴파일을 통해 발생하는 UnbondingCompleted 이벤트를 구독하세요. * [**시스템 모듈 레퍼런스**](/ko/reference/system-modules-api-overview) — 모듈별 ABI, 메서드 시그니처, 이벤트 스키마로 바로 이동하세요. * [**시스템 모듈 개념**](/ko/explanation/system-modules-overview) — Stable이 SDK 모듈을 프리컴파일을 통해 노출하는 이유를 이해하세요. ## 스마트 컨트랙트 검증하기 검증은 컨트랙트의 소스 코드를 블록 탐색기에 업로드하고 배포된 바이트코드로 컴파일됨을 증명합니다. 검증이 완료되면, 사용자는 코드를 재호스팅하지 않고도 Stablescan에서 상태를 읽고, 함수를 호출하고, 소스를 감사할 수 있습니다. 이 가이드는 Stable에서 Foundry로 배포된 컨트랙트를 검증하는 과정을 안내합니다. ### 사전 준비 * Stable 테스트넷 또는 메인넷에 이미 배포된 컨트랙트. 아직 배포하지 않았다면 [스마트 컨트랙트 배포하기](/ko/tutorial/smart-contract)를 참조하세요. * Foundry 설치 (PATH에서 `forge` 사용 가능). * `forge create` 출력에서 얻은 배포된 컨트랙트 주소. ### 1. 배포된 주소 확인 이전 배포에서 얻은 `Deployed to` 주소가 있는지 확인하세요. [스마트 컨트랙트 배포하기](/ko/tutorial/smart-contract) 흐름에서는 `forge create` 이후에 출력된 값입니다. ```bash cast code 0xDeployedContractAddress --rpc-url https://rpc.testnet.stable.xyz | head -c 20 ``` ```text 0x6080604052600436... ``` 비어 있지 않은 바이트코드는 해당 주소에 컨트랙트가 배포되었음을 확인해 줍니다. ### 2. forge verify-contract 실행 Foundry의 검증 흐름은 소스를 Stablescan 검증기에 제출합니다. ```bash forge verify-contract \ 0xDeployedContractAddress \ src/Counter.sol:Counter \ --chain-id 2201 \ --verifier blockscout \ --verifier-url https://testnet.stablescan.xyz/api \ --watch ``` ```text Start verifying contract `0xDeployedContractAddress` deployed on 2201 Submitting verification of contract: Counter Submitted contract for verification: Response: `OK` GUID: `abc123...` URL: https://testnet.stablescan.xyz/address/0xDeployedContractAddress Contract verification status: Response: `OK` Details: `Pass - Verified` Contract successfully verified ``` `--watch`는 검증이 완료될 때까지 차단하므로 폴링할 필요가 없습니다. 메인넷에서는 chain ID를 `988`로, 검증기 URL을 `https://stablescan.xyz/api`로 바꾸세요. :::note **생성자 인자**: 컨트랙트가 생성자 인자를 받는 경우, 명령어에 `--constructor-args $(cast abi-encode "constructor(uint256,address)" 42 0xSomeAddress)`를 추가하세요. 이 플래그가 없으면 비어 있지 않은 생성자를 가진 모든 컨트랙트의 검증이 실패합니다. ::: ### 3. Stablescan에서 검증 확인 탐색기에서 컨트랙트 페이지를 엽니다. ```text https://testnet.stablescan.xyz/address/0xDeployedContractAddress ``` 이제 **Contract** 탭에 소스 코드, 녹색 "Verified" 배지, 그리고 전체 ABI가 표시되어야 합니다. 사용자는 **Read Contract**에서 상태를 읽고 **Write Contract**에서 트랜잭션을 보낼 수 있습니다. ### 문제 해결 * **"Bytecode does not match"**: 소스가 배포된 것과 다른 바이트코드로 컴파일됩니다. 대부분 일치하지 않는 Solidity 버전이나 옵티마이저 설정으로 인해 발생합니다. `foundry.toml`과 일치하도록 `--compiler-version`과 `--optimizer-runs`를 명시적으로 전달하세요. * **"GUID not found"**: 검증기가 아직 제출을 등록하지 않았습니다. `--watch`로 다시 실행하거나 응답에 출력된 URL을 수동으로 확인하세요. * **컨트랙트가 라이브러리를 사용하는 경우**: 링크된 각 라이브러리에 대해 `--libraries src/Lib.sol:Lib:0xDeployedLibAddress`를 추가하세요. ### 다음 추천 * [**컨트랙트 이벤트 인덱싱**](/ko/how-to/index-contract) — ethers.js로 온체인 이벤트를 구독하고 실시간 이벤트 스트림을 구축하세요. * [**스마트 컨트랙트 배포하기**](/ko/tutorial/smart-contract) — 새로운 Foundry 프로젝트를 구성하고 Stable 테스트넷에 배포하세요. * [**JSON-RPC 레퍼런스**](/ko/reference/json-rpc-api) — Stable이 온체인 상호작용을 위해 지원하는 `eth_*` 메서드를 확인하세요. ## USDT0를 가스로 사용하기 Stable에서 USDT0는 체인의 네이티브 자산이자 ERC-20 토큰입니다. 가스 토큰은 별도의 네이티브 자산이 아닌 USDT0입니다. 표준 이더리움 가스 추정은 세 가지를 조정하면 그대로 작동합니다: `maxPriorityFeePerGas`는 항상 `0`이고, `baseFee`는 USDT0로 표기되며, 네이티브 전송에서 `value` 필드는 ETH가 아닌 USDT0를 담습니다. 이 가이드는 Stable에서 트랜잭션을 올바르게 구성하는 방법과 이더리움 코드를 이식할 때 무엇을 변경해야 하는지 보여줍니다. ### 이더리움과의 차이점 | **필드** | **이더리움** | **Stable** | | :-------------------------------- | :--------- | :------------- | | 가스 토큰 | ETH | USDT0 | | `maxPriorityFeePerGas` | 순서 지정에 사용됨 | 무시됨 (`0`으로 설정) | | `baseFeePerGas` | ETH로 표기 | USDT0로 표기 | | `value` (네이티브 전송) | ETH 전송 | USDT0 전송 | | EIP-1559 트랜잭션 형식 | 지원됨 | 지원됨 | | `eth_estimateGas`, `eth_gasPrice` | 지원됨 | 지원됨 | | `eth_maxPriorityFeePerGas` | 팁 반환 | `0` 반환 | 트랜잭션 형식이 변경되지 않으므로 기존 ethers.js, viem, Hardhat, Foundry 코드는 변경 없이 Stable에서 실행됩니다. 차이는 가스 필드를 *인코딩*하는 방식이 아니라 *계산*하는 방식에 있습니다. ### 트랜잭션 구성하기 base fee를 가져오고, `maxPriorityFeePerGas`를 `0`으로 설정하고, 안전 마진으로 base fee를 두 배로 합니다. ```typescript // sendNative.ts import { ethers } from "ethers"; import "dotenv/config"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const maxPriorityFeePerGas = 0n; // always 0 on Stable const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // 2x headroom const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: ethers.parseEther("0.001"), // 0.001 USDT0, 18 decimals maxFeePerGas, maxPriorityFeePerGas, }); const receipt = await tx.wait(1); console.log("Tx:", receipt!.hash); console.log("Gas used:", receipt!.gasUsed.toString()); console.log("Effective gas price:", receipt!.gasPrice.toString(), "(USDT0 wei-equivalent)"); ``` ```bash npx tsx sendNative.ts ``` ```text Tx: 0x8f3a...2d41 Gas used: 21000 Effective gas price: 1000000000 (USDT0 wei-equivalent) ``` 실효 가스 가격은 USDT0로 표기된 값입니다. `1 gwei`에서 21,000 가스 네이티브 전송은 약 `0.000021` USDT0의 비용이 듭니다. ### USDT0로 가스 비용 추정하기 `eth_estimateGas`와 `eth_gasPrice`는 이더리움과 동일하게 동작합니다. 가스 토큰이 USDT0이므로 결과는 이미 USDT0 단위입니다. ```typescript // estimate.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const gasPrice = await provider.send("eth_gasPrice", []); const gasEstimate = await provider.estimateGas({ to: "0xContractAddress", data: "0x...", }); const feeInUSDT0 = BigInt(gasPrice) * gasEstimate; console.log("Estimated fee:", ethers.formatEther(feeInUSDT0), "USDT0"); ``` ```bash npx tsx estimate.ts ``` ```text Estimated fee: 0.000021 USDT0 ``` :::warning `eth_maxPriorityFeePerGas`는 Stable에서 항상 `0`을 반환합니다. 지갑이나 SDK가 RPC가 반환한 priority fee를 base fee 위에 추가하더라도 여전히 작동하지만, 별도의 팁을 표시하는 수수료 UI는 `0`을 표시하므로 숨겨야 합니다. ::: ### 툴링 구성 * **Hardhat / Foundry**: 특별한 구성이 필요 없습니다. 표준 EVM 설정이 작동합니다. 구성에서 priority fee를 명시적으로 설정한다면 `0`으로 설정하세요. * **지갑**: priority 팁 입력 필드를 숨기거나 비활성화하세요. 이 값은 순서 지정이나 포함에 영향을 미치지 않으므로 표시하는 것은 오해를 일으킵니다. * **모니터링**: 수수료 분석 대시보드는 priority fee를 차트로 표시하지 않아야 합니다. Stable에서는 항상 0입니다. ### 이더리움에서 이식할 때 흔한 실수 * **ETH로 표기된 팁 적용**: 이더리움에서 priority-fee 상수를 복사한다고 해서 더 빠른 포함이 이루어지지 않습니다. Stable은 base fee만으로 트랜잭션 순서를 정합니다. * **`value`를 ETH로 취급**: 네이티브 전송의 `value`는 USDT0입니다. ETH/USD 가격으로 변환하지 마세요. * **수수료 상한 하드코딩**: 고정 값 대신 실시간 `baseFeePerGas`로부터 `maxFeePerGas`를 설정하세요(예: `baseFee * 2`). 그래야 base fee가 상승할 때 트랜잭션이 멈추지 않습니다. ### 다음 추천 * [**가스 가격 책정 레퍼런스**](/ko/reference/gas-pricing-api) — 전체 base-fee 모델, EIP-1559 형식, `eth_*` 메서드 동작. * [**제로 가스 트랜잭션**](/ko/how-to/zero-gas-transactions) — 애플리케이션이 Gas Waiver를 통해 가스를 부담하도록 하기. * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 잔액 조정 및 USDT0의 이중 역할을 고려한 컨트랙트 설계. ## 가스리스 트랜잭션 Gas Waiver를 사용하면 애플리케이션이 사용자를 대신해 가스를 부담할 수 있습니다. 사용자는 `gasPrice = 0`으로 트랜잭션에 서명하고, 거버넌스에 등록된 waiver가 이를 감싸며, 검증자는 사용자에게 비용이 들지 않게 호출을 실행합니다. 이 가이드는 조건을 충족하는 전송 과정을 살펴보고, 가스가 면제되었는지 확인하는 방법을 보여주며, waiver가 무엇을 다루고 다루지 않는지 설명합니다. :::note **개념**: 래퍼 트랜잭션 메커니즘, 권한 모델, 보안 보장에 대해서는 [Gas waiver](/ko/explanation/gas-waiver)와 [Gas waiver 프로토콜 레퍼런스](/ko/reference/gas-waiver-api)를 참조하세요. ::: ### 무엇을 만들 것인가 호스팅된 Waiver 서버를 통해 USDT0 전송을 제출하고, 영수증을 가져온 다음, `gasPrice = 0`을 확인하는 두 개의 스크립트 흐름입니다. #### 데모 ```text step 1. Connect wallet, balance displayed as 0.01 USDT0 step 2. Send transaction via Gas Waiver → [Run] step 3. Result tx: 0x8f3a...2d41 Gas fee paid by you: 0.000000 USDT0 Balance after: 0.01 USDT0 ``` ### waiver가 적용되는 경우 다음 조건이 모두 충족되면 트랜잭션이 자격을 갖춥니다: * 사용자가 `gasPrice = 0`으로 내부 트랜잭션에 서명합니다. * 제출자가 거버넌스에 등록된 waiver 주소입니다. * 대상 `to` 주소와 메서드 셀렉터가 waiver의 `AllowedTarget` 정책에 있습니다. * 래퍼가 `value = 0` 및 `gasPrice = 0`으로 마커 주소 `0x000000000000000000000000000000000000f333`에 전송됩니다. 이 중 하나라도 실패하면, 검증자는 내부 호출을 실행하지 않고 래퍼를 거부합니다. `AllowedTarget`에 등록되지 않은 컨트랙트 호출은 다루지 않습니다. 임의의 셀프 서비스 waiver는 불가능하며, 모든 waiver는 검증자 거버넌스를 통해 등록되어야 합니다. ### 사전 요구 사항 * Stable 팀이 발급한 Waiver 서버용 API 키. * waiver의 `AllowedTarget` 정책에 등록된 대상 컨트랙트 주소와 메서드 셀렉터. * 가스용 USDT0가 필요 없는 테스트넷의 사용자 지갑. ### 1단계: 자격을 갖춘 InnerTx 서명하기 사용자는 `gasPrice = 0`으로 표준 트랜잭션에 서명합니다. 이 예제에서 호출은 USDT0 `transfer`이며, 이는 애플리케이션이 가스를 부담하는 흐름에서 흔히 사용되는 `AllowedTarget`입니다. ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet WAIVER_SERVER: "https://waiver.testnet.stable.xyz", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; export const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); export const userWallet = new ethers.Wallet(process.env.USER_PRIVATE_KEY!, provider); ``` ```typescript // signInner.ts import { ethers } from "ethers"; import { CONFIG, provider, userWallet } from "./config"; const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ "0xRecipientAddress", ethers.parseUnits("0.001", 18), ]); const gasLimit = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit, nonce, chainId: CONFIG.CHAIN_ID, }; export const signedInnerTx = await userWallet.signTransaction(innerTx); console.log("Signed InnerTx:", signedInnerTx); ``` ```bash npx tsx signInner.ts ``` ```text Signed InnerTx: 0xf8a8...c1 ``` :::warning `gasPrice`는 반드시 `0`이어야 합니다. 0이 아닌 값은 waiver 서버가 제출을 거부하고 검증자가 래퍼를 거부하게 만듭니다. ::: ### 2단계: Waiver 서버를 통해 제출하기 Waiver 서버는 서명된 내부 트랜잭션을 감싸서 브로드캐스트합니다. 서버에서 발급한 API 키가 필요합니다. ```typescript // submit.ts import { CONFIG } from "./config"; import { signedInnerTx } from "./signInner"; const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.WAIVER_API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTx] }), }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let txHash = ""; while (true) { const { done, value } = await reader.read(); if (done) break; for (const line of decoder.decode(value).trim().split("\n")) { const result = JSON.parse(line); if (result.success) { txHash = result.txHash; console.log(`tx confirmed: ${txHash}`); } else { console.error(`tx failed: ${result.error.message}`); } } } export { txHash }; ``` ```bash npx tsx submit.ts ``` ```text tx confirmed: 0x8f3a...2d41 ``` ### 3단계: 영수증에 가스가 0으로 표시되는지 확인하기 영수증을 가져와 `effectiveGasPrice`가 0인지 확인합니다. 이것이 사용자가 가스를 지불하지 않았다는 암호학적 증거입니다. ```typescript // verify.ts import { provider } from "./config"; import { txHash } from "./submit"; const receipt = await provider.getTransactionReceipt(txHash); const gasUsed = receipt!.gasUsed; const effectiveGasPrice = receipt!.gasPrice; const totalFee = gasUsed * effectiveGasPrice; console.log("Gas used: ", gasUsed.toString()); console.log("Effective gas price:", effectiveGasPrice.toString()); console.log("Gas fee paid: ", `${totalFee.toString()} USDT0 (wei-equivalent)`); ``` ```bash npx tsx verify.ts ``` ```text Gas used: 21000 Effective gas price: 0 Gas fee paid: 0 USDT0 (wei-equivalent) ``` `effectiveGasPrice`가 `0`이면 트랜잭션이 등록된 waiver 하에서 실행되었으며 사용자에게 요금이 부과되지 않았음을 확인할 수 있습니다. ### Gas Waiver가 다루지 않는 것 * **`AllowedTarget` 외부의 컨트랙트**: 임의의 컨트랙트 호출은 다루지 않습니다. 모든 대상은 거버넌스를 통해 waiver별로 범위가 지정됩니다. * **사용자가 제출한 래퍼**: 사용자가 `0x...f333`에 직접 제출하면 실패합니다. 등록된 waiver 주소만 래핑할 수 있습니다. * **수수료 추출**: 검증자는 내부 또는 래퍼 트랜잭션 어느 쪽에서도 0이 아닌 `gasPrice`를 허용하지 않습니다. 전체 정책 모델과 waiver별 범위 규칙은 [Gas waiver 프로토콜](/ko/reference/gas-waiver-api)을 참조하세요. ### 다음 추천 사항 * [**Waiver 서버 통합하기**](/ko/how-to/integrate-gas-waiver) — 전체 API 레퍼런스, 배치 제출, 오류 코드, NDJSON 스트리밍. * [**셀프 호스팅 Gas Waiver**](/ko/how-to/self-hosted-gas-waiver) — 자체 waiver 주소를 등록하고 호스팅된 API 없이 래퍼를 브로드캐스트합니다. * [**Gas waiver 프로토콜**](/ko/reference/gas-waiver-api) — 전체 명세를 읽어보세요: 마커 라우팅, 래퍼 형식, 거버넌스 제어. ## 계정 가이드 계정 탭 아래의 모든 가이드, 개념, 참조를 수행하려는 작업별로 분류했습니다. ### 지갑 설정 * [**지갑 생성하기**](/ko/how-to/create-wallet) — ethers.js 또는 Tether WDK를 사용하여 새 키 쌍을 생성하거나 시드 구문에서 복원합니다. * [**에이전트 지갑**](/ko/reference/agentic-wallets) — AI 에이전트를 위한 자가 수탁형 지갑 — 사용자 지갑과의 차이점. ### 계정 위임 (EIP-7702) * [**EIP-7702 개념**](/ko/explanation/eip-7702) — Stable에서 EIP-7702가 가능하게 하는 것과 보안 모델. * [**계정 추상화 사용법**](/ko/how-to/account-abstraction) — EIP-7702를 일괄 결제, 지출 한도, 세션 키에 적용합니다. ### 참조 * [**EIP-7702 API**](/ko/reference/eip-7702-api) — Type-4 트랜잭션 형식과 권한 부여 목록. * [**구독 및 수금**](/ko/how-to/subscribe-and-collect) — EIP-7702 위임을 구독 결제 흐름에 적용합니다 (교차 게시됨). ## Stable의 계정 Stable의 계정은 [EIP-7702 위임](/ko/explanation/eip-7702)을 통해 선택적으로 스마트 컨트랙트 로직을 실행할 수 있는 표준 이더리움 EOA입니다. 사용자는 지갑, 일괄 결제, 정기 구독, 세션 키 전반에 걸쳐 하나의 주소와 하나의 개인 키를 유지합니다. 에이전트는 어떠한 커스터디 미들웨어 없이 동일한 계정 모델을 실행합니다. ### 무엇을 구축할 수 있나요 * 네이티브 USDT0 잔액 조회와 서명된 트랜잭션을 갖춘, 시드 문구로부터의 **지갑**. * **일괄 결제**: 위임된 EOA를 통해 하나의 원자적 트랜잭션으로 여러 전송을 실행합니다. * **지출 한도**: 위임 로직을 통해 EOA 자체에 트랜잭션당 또는 일일 상한을 적용합니다. * **세션 키**: 범위가 제한되고 시간이 제한되며 예산이 제한된 키를 dApp에 부여하여 사용자가 모든 작업마다 다시 서명할 필요가 없도록 합니다. * **에이전트 지갑**: 자체 보관 키로 AI 에이전트에 자금을 공급하고 x402 서비스 비용을 자율적으로 지불하게 합니다. 제공자 및 통합 패턴은 [에이전트 지갑](/ko/reference/agentic-wallets)을 참조하세요. ### Stable이 다른 점 * **모든 것을 위한 하나의 주소.** 스마트 컨트랙트 기능을 사용하기 위한 계정 마이그레이션이 필요 없습니다. EIP-7702는 기존 EOA *위에* 코드를 위임합니다. * **USDT0 전용 가스.** 사용자는 별도의 네이티브 토큰이 필요 없습니다. 새 계정은 USDT0로 자금을 공급하고 즉시 트랜잭션을 처리할 수 있습니다. * **다기능 위임 패턴.** 단일 위임자가 일괄 처리, 지출 한도, 세션 키, 구독을 결합할 수 있어 하나의 위임으로 출시하는 모든 기능을 커버합니다. ### 여기서 시작하세요 * [**지갑 만들기**](/ko/how-to/create-wallet) — ethers.js 또는 Tether WDK로 지갑을 생성하거나 복원합니다. * [**EIP-7702로 위임하기**](/ko/how-to/account-abstraction) — 기존 EOA에 일괄 결제, 지출 한도, 세션 키를 적용합니다. * [**Stable SDK**](/ko/explanation/sdk-overview) — 타입이 지정된 클라이언트를 사용하여 모든 계정에서 트랜잭션을 서명하고 전송합니다. ### 다음 추천 * [**계정 가이드 색인**](/ko/explanation/accounts-guides) — 계정 가이드 및 참조의 전체 목록으로 이동합니다. * [**EIP-7702 개념**](/ko/explanation/eip-7702) — 계정 마이그레이션 없이 위임이 작동하는 이유. * [**구독 및 수금**](/ko/how-to/subscribe-and-collect) — 계정 모델을 정기 결제 흐름에 적용합니다. ## 에이전트 정산 에이전트 정산은 머신 결제를 위한 Stable의 레일입니다. 에이전트는 USDT0 잔액을 보유하고, HTTP를 통해 리소스 비용을 지불하며, 결제는 동일한 요청 주기 안에서 온체인으로 정산됩니다. 에이전트는 결제와 네트워크 수수료 모두를 하나의 잔액에서 차감합니다. 별도의 가스 토큰도, 가입도, API 키 교체도 없습니다. ### 이것이 에이전트에게 중요한 이유 에이전트는 사람과 다르게 거래합니다. 에이전트는 지속적으로 실행되고, 소액 결제를 여러 번 수행하며, 가입 절차를 완료하거나 API 키를 교체할 수 없습니다. Stable의 정산은 이러한 작업 부하에 부합합니다: * **USDT0는 가스 토큰이자 결제 토큰입니다.** 에이전트 지갑은 단일 자산을 보유하고 수수료와 결제 모두에 이를 차감합니다. * **1센트 미만의 예측 가능한 수수료.** 수수료는 달러로 표시되므로, 에이전트는 변동성 있는 가스 자산을 환산하지 않고도 작업당 비용을 예산화할 수 있습니다. * **1초 미만의 완결성.** 유료 HTTP 호출은 요청 수명 주기(블록 시간 약 700ms) 안에서 정산되며, 이는 고빈도 머신 트래픽을 실현 가능하게 만듭니다. * **USDT 보급.** USDT는 가장 널리 보유된 스테이블코인이며, Stable은 이를 위해 특별히 구축된 플랫폼입니다. ### 레이어가 어떻게 맞물리는가 두 레이어는 서로 다른 작업을 수행하며 대안이 아니라 상호 보완적입니다: * **x402**는 *결제 표준*입니다. 서버가 `402 Payment Required`로 응답하고, 클라이언트가 인가에 서명하며, 퍼실리테이터가 이를 제출하는 HTTP 네이티브 프로토콜입니다. * \*\*MPP (Machine Payments Protocol)\*\*는 더 광범위한 인텐트와 멀티 레일 지원으로 x402를 대체하는 IETF 트랙 표준입니다. x402는 Stable이 오늘날 지원하는 하위 호환 부분 집합입니다. [MPP](/ko/explanation/mpp)를 참조하세요. * **Stable**은 *정산 레이어*입니다. USDT0의 온체인 전송이 실제로 일어나는 곳입니다. **퍼실리테이터**는 이 둘 사이에 위치합니다: 서명된 결제를 검증하고 온체인 호출을 제출하여 개발자가 정산 인프라를 운영하지 않아도 되도록 합니다. 오늘날 Stable을 지원하는 제공자는 [퍼실리테이터](/ko/reference/agentic-facilitators)를 참조하세요. ```text agent (client) ──HTTP──▶ resource server ──signed payment──▶ facilitator ──tx──▶ Stable (returns 402) (verify + submit) (USDT0 settles) ``` ### 무엇을 만들 수 있는가 * **호출당 지불 API** — x402 또는 MPP를 통해 정산되며 요청당 USDT0로 가격이 책정됩니다. * **에이전트 간 상거래** — 한 에이전트가 HTTP를 통해 서비스 비용을 다른 에이전트에게 지불합니다. * **유료 MCP 도구** — x402 엔드포인트를 래핑하여 AI 클라이언트가 프롬프트를 통해 이를 호출하고 비용을 지불합니다. * **자율 조달** — 예산이 책정된 USDT0 잔액에 대해 수행됩니다. * **사용량 기반 청구** — 인보이스당이 아니라 요청당 정산됩니다. * **에이전트 지갑** — USDT0만으로 자금이 충전되며, 수탁 미들웨어가 없습니다. ### 여기서 시작하세요 * [**호출당 지불 API 만들기**](/ko/how-to/build-pay-per-call) — x402로 게이팅된 엔드포인트를 구축하고 요청 내에서 실제 USDT0 결제를 정산합니다. * [**Stable에서 MPP 엔드포인트 만들기**](/ko/how-to/build-mpp-endpoint) — USDT0를 위한 세 가지 MPP 커스텀 메서드 훅을 작성하고 Stable에서 정산합니다. * [**AI로 개발하기**](/ko/how-to/develop-with-ai) — Docs MCP와 Runtime MCP를 AI 에디터에 연결하고 Stable 컨텍스트 블록을 붙여넣습니다. * [**MCP 서버로 지불하기**](/ko/how-to/pay-with-mcp) — x402 유료 API를 에이전트가 자연어 프롬프트를 통해 호출할 수 있는 MCP 도구로 노출합니다. ### 다음 추천 * [**x402 심층 분석**](/ko/explanation/x402) — HTTP 결제 프로토콜이 Stable에서 처음부터 끝까지 어떻게 작동하는지 읽어보세요. * [**MPP**](/ko/explanation/mpp) — x402가 속한 더 광범위한 IETF 트랙 표준입니다. * [**퍼실리테이터**](/ko/reference/agentic-facilitators) — 어떤 퍼실리테이터가 이미 Stable에서 USDT0 결제를 정산하는지 확인하세요. ## AI 및 에이전트 가이드 AI/에이전트 탭 아래의 모든 가이드, 개념 및 참조를 하려는 작업별로 그룹화했습니다. ### AI 에디터 준비하기 * [**AI로 개발하기**](/ko/how-to/develop-with-ai) — Docs MCP, Runtime MCP, 에이전트 스킬을 설치하고 Stable 컨텍스트 블록을 붙여넣으세요. * [**에이전트 지갑 만들기**](/ko/how-to/create-wallet) — WDK를 통한 자체 보관 키 — 에이전트 결제의 기반입니다. ### 서비스 수익화 및 소비하기 * [**호출당 과금 API 만들기**](/ko/how-to/build-pay-per-call) — x402 미들웨어로 모든 HTTP 엔드포인트에 USDT0로 요청당 가격을 책정하세요. * [**MCP 서버로 결제하기**](/ko/how-to/pay-with-mcp) — x402 유료 API를 MCP 도구로 래핑하여 AI 클라이언트가 호출하고 결제하도록 하세요. ### 참조 * [**에이전트 퍼실리테이터**](/ko/reference/agentic-facilitators) — Stable에서 에이전트 간 상거래를 위한 정산 서비스입니다. * [**에이전트 지갑**](/ko/reference/agentic-wallets) — 자율 에이전트 사용을 위한 지갑 사양입니다. ### 기본 개념 * [**x402 (HTTP 네이티브 결제)**](/ko/explanation/x402) — 에이전트가 요청당 결제에 사용하는 HTTP 프로토콜입니다. * [**MPP**](/ko/explanation/mpp) — x402가 속한 더 넓은 IETF 트랙 표준으로, 세션 및 멀티 레일 지원을 제공합니다. * [**ERC-3009**](/ko/explanation/erc-3009) — x402가 정산하는 서명 승인 표준입니다. ## Autobahn ### BFT 내 트레이드오프: 레이턴시 vs. 견고성 현대의 비잔틴 장애 허용(Byzantine Fault Tolerant, BFT) 합의 프로토콜은 일반적으로 부분 동기(partial synchrony) 모델에서 동작합니다. 이 모델은 어떤 시점 이후에는 네트워크가 안정되며 메시지 지연 시간이 유한하게 유지된다는 가정에 기반합니다. 이 모델은 프로토콜 설계에 있어 실용적으로 작용해 왔지만, 실제 운영 환경에서는 장기간 상태를 안정적으로 유지하기 어렵습니다. 대신 시스템은 지속적인 동기 구간과 간헐적인 장애(예: 레이턴시 증가, 노드 장애, 공격적 조건 등)를 반복적으로 겪습니다. 이러한 일시적 장애를 **"블립(blip)"** 이라고 합니다. 이러한 조건 하에서, 기존 합의 프로토콜은 **안정적인 네트워크 환경에서의 저지연성과 장애 발생 시의 견고성 사이에서 선택**을 강요받습니다. * PBFT, HotStuff와 같은 전통적인 **view 기반 BFT 프로토콜**은 네트워크가 안정되어 있을 때 좋은 반응성을 갖추도록 최적화되어 있습니다. 그러나 블립이 발생하면 성능이 급격히 저하되며, 이 성능 저하의 여파로 네트워크가 회복된 후에도 백로그된 요청들이 누적되어 트랜잭션 처리가 지연될 수 있습니다. * [Narwhal & Tusk](https://arxiv.org/pdf/2105.11827)/[Bullshark](https://arxiv.org/pdf/2201.05677)와 같은 **DAG 기반 BFT 프로토콜**은 데이터 전파(DAG)와 합의(BFT)를 분리하여, 트랜잭션을 비동기적으로 각 레플리카에 전파합니다. 이러한 설계는 높은 처리량을 가능하게 하며, 네트워크 장애 중에도 시스템이 계속 진행(progress)할 수 있도록 합니다. 하지만 비동기 정렬 메커니즘의 복잡성으로 인해, 안정된 상황에서도 높은 레이턴시를 보이는 경향이 있습니다. [**Autobahn**](https://arxiv.org/pdf/2401.10369)은 이러한 두 설계 철학을 연결하는 새로운 접근 방식을 제시합니다. Autobahn은 DAG 기반 프로토콜의 높은 처리량 및 블립 내성(blip tolerance)과, 전통적인 view 기반 합의의 낮은 레이턴시를 결합합니다. Autobahn의 핵심은 데이터 전파 계층이 합의의 진행 여부와 무관하게 지속적으로 제안을 네트워크 속도로 전파하는 고도로 병렬화된 구조입니다. 이를 기반으로 Autobahn은 낮은 레이턴시의 부분 동기 합의 프로토콜을 운영하며, 데이터 레이어 위에 올라가는 경량 스냅샷을 참조하여 프로포절을 커밋합니다. Autobahn의 결정적인 특징은, 블립 이후에도 성능 저하 없이 복구가 될 수 있다는 점입니다. 이러한 속성은 “**seamlessness**“라 불리며, 네트워크가 안정화되면 누적된 트랜잭션 백로그를 재처리 없이 즉시 커밋할 수 있도록 보장합니다. 데이터의 가용성과 순서 정렬을 명확히 분리하고, 프로토콜에 의한 동기화 지연을 방지함으로써, Autobahn은 현실적 블록체인 환경에서 강력하면서도 반응성이 뛰어난 합의 기반을 제공합니다. ### Autobahn 아키텍처 개요 Autobahn은 두 핵심 계층 - **데이터 전파 계층**과 **합의 계층**의 책임을 명확히 분리한 구조 위에 설계되어 있습니다. 이러한 분리는 Narwhal과 같은 DAG 시스템에서 영감을 받았으며, Autobahn은 여기에 seamlessness와 낮은 레이턴시를 지원하기 위한 확장을 더했습니다. 데이터 전파 계층은 클라이언트 트랜잭션을 확장 가능하고 비동기적인 방식으로 브로드캐스트하는 역할을 합니다. 각 레플리카는 각자 트랜잭션 묶음을 전파하는 별도의 ‘차선(lane)’을 가지고 있으며, 이는 합의 상태와 무관하게 독립적으로 전파 및 인증될 수 있습니다. 각 차선은 합의가 지연되더라도 지속적으로 유지되므로, 시스템은 항상 클라이언트에게 반응성을 유지합니다. 이 위에서 Autobahn은 PBFT 스타일의 부분 동기 합의 계층을 운영합니다. 여기서 Autobahn은 각 트랜잭션 묶음이 아닌, 모든 데이터 차선의 ‘최신 상태 요약(tip cuts)’에 대해 합의를 수행합니다. 이러한 설계는 임의의 큰 데이터셋도 단일한 과정으로 커밋할 수 있게 하며, 블립의 영향을 최소화합니다. 데이터와 합의를 강하게 결합하여 리더가 실패하면 멈추는 Hotstuff나, DAG 순회 및 데이터 동기화로 인해 높은 커밋 레이턴시를 겪는 Bullshark와 비교했을때, Autobahn은 더 유연하고 빠른 합의 경험을 제공합니다. 즉, Autobahn은 DAG의 병렬화 속성을 가져가면서 레이턴시 단점은 개선한 구조입니다. ### 데이터 전파 계층: 차선과 Car ![Autobahn: Seamless high speed BFT](/images/autobahn-high-speed1.png) *Autobahn: Seamless high speed BFT* Autobahn에서는 각 레플리카가 자신만의 **차선(Lane)** 에 트랜잭션을 제안합니다. 각 데이터 프로포절은 다른 레플리카 노드들의 승인(Acknowledgment)과 함께 묶여 하나의 **‘Car’**(Certification of Available Request)를 형성합니다. 이 Car들은 최소 하나의 레플리카 노드가 해당 데이터를 들고 있다는 데이터 가용성 증명(Proof of Availability, PoA) 역할을 수행합니다. 각 Car는 이전 Car를 참조하여 체인처럼 연결되므로, 차선의 최신 블록을 검증하는 것만으로도 차선 전체의 데이터 가용성을 간접적으로 증명할 수 있습니다. 이러한 형태의 연쇄적인 가용성 증명은 즉각적인 참조를 가능하게 합니다. 여기서 즉각적인 참조란, 합의 계층이 DAG 순회 없이도 최신 상태 요약(tip cut, 현재 차선 헤드의 벡터)을 참조할 수 있으며, 모든 이전 데이터에 접근할 수 있음을 알 수 있다는 의미입니다. 일반적인 DAG 프로토콜과 달리, Autobahn은 글로벌 가용성과 비중복성을 보장하기 위한 비용이 많이 드는 신뢰 가능한 브로드캐스트 단계를 피합니다. 대신, 최소한의 조율만을 사용하고, PoA(Proof of Availability)마다 하나 이상의 정직한 복제본이 데이터를 보유하고 있다고 신뢰합니다. 이를 통해 가변적인 부하나 부분적인 장애 상황에서도 높은 처리량과 낮은 지연 시간을 유지할 수 있습니다. 데이터 계층은 합의와 독립적으로 계속 진행되며, 일시적인 장애(blip) 상황에서도 시스템의 반응성을 보장합니다. ### **합의 레이어: 저지연 동의** ![Autobahn: Seamless high speed BFT](/images/autobahn-high-speed2.png) *Autobahn: Seamless high speed BFT* Autobahn의 합의 계층은 기존 PBFT 원칙을 기반으로 하지만, 레이턴시 감소 및 원활한 복구를 위한 핵심 최적화 메커니즘을 도입합니다. 각 합의 슬롯은 모든 레플리카의 차선에서 가장 최신의 인증된 제안들을 요약한 "**tip cut**"을 커밋 대상으로 삼습니다. 합의 리더는 이 tip cut을 2단계 커밋 프로세스(Prepare → Confirm)를 통해 제안합니다. Prepare 단계에서는, 레플리카들이 제안된 tip cut에 투표합니다. 리더가 빠르게 정족수(quorum)만큼의 투표를 받으면, Fast Path로 진입하여 세 단계의 메시지 레이턴시 안에 커밋할 수 있습니다. 만약 투표를 빠르게 모으지 못했다면, Confirm 단계로 넘어갑니다. 여기서는 다시 레플리카들의 승인을 모아 여섯 단계의 메세지 레이턴시 내에 커밋을 완료합니다. Autobahn의 핵심 혁신은, 데이터 동기화와 합의 투표의 분리입니다. 레플리카들은 전체 프로포절 데이터를 받지 못하였더라도, 인증된 tip에 대해서만 투표할 수 있습니다. PoA가 데이터 가용성을 보장해주기 때문에, tip에 대해서만 투표하더라도 안전하다고 볼 수 있는 것입니다. 동기화는 병렬로 진행되어 실행 단계 전에 완료되고, 이를 통해 프로토콜이 멈추는 것을 방지할 수 있습니다. 리더 실패 혹은 타임아웃의 경우, 타임아웃 인증서(timeout certificate)를 이용해 뷰 변경(view change)을 트리거하는 방식으로 새 리더가 작업을 이어갈 수 있습니다. ### Autobahn의 핵심 속성 Autobahn은 BFT 프로토콜이 제공해야 하는 표준적인 **안전성(safety)** 과 **생존성(liveness)** 보장을 충족합니다. 안전성은 두 개의 올바른 레플리카가 같은 슬롯에 대해 서로 다른 블록을 커밋하는 일이 없음을 보장하며, 생존성은 글로벌 동기화 시간(Global Stabilization Time, GST) 이후, 올바른 리더가 선정되면 합의가 진행됨을 보장합니다. Autobahn의 가장 중요한 속성은 **seamlessness**입니다. 이 특성 덕분에 합의 레이어는 임의의 크기를 가진 백로그도 상수 시간 안에 커밋할 수 있고, 프로토콜 성능 저하를 막습니다. 블립이 발생하더라도 이후 네트워크가 안정화되면, 성공적으로 전파된 모든 제안은 즉시 커밋됩니다. 이를 통해 Autobahn은 간헐적 장애 환경에서도 부드럽게 동작하며, 회복 속도와 반응성 측면에서 전통적인 BFT 프로토콜을 능가합니다. 또한, 프로토콜은 **수평 확장성**을 가집니다. 각 레플리카는 자신의 차선을 통해 전체 처리량에 기여하며, 합의 스냅샷(consensus cut) 역시 참여자 수에 따라 자연스럽게 확장됩니다. 이는 높은 성능과 견고함을 모두 요구하는 대규모 블록체인 배포에 적합합니다. ### 낮은 레이턴시와 높은 복원력의 조화 Autobahn은 주요 BFT 프로토콜인 Bullshark 및 HotStuff와 비교 테스트되었습니다. 테스트 환경은 이상적인 조건과 장애 삽입 조건 모두를 포함합니다. 결과적으로 Autobahn은 모든 테스트에서 가장 좋은 결과를 달성했습니다: Autobahn은 Bullshark 수준의 처리량(초당 230,000건 이상)을 유지하면서, 레이턴시는 50% 이상 감소시켰습니다. 좋은 네트워크 환경에서, Autobahn은 3\~6개의 메시지 레이턴시만으로 트랜잭션을 커밋하며, 이는 Bullshark의 12개 메시지 레이턴시보다 훨씬 낮습니다. 실제로 Autobahn은 약 280ms, Bullshark는 590ms 이상의 커밋 지연이 측정되었습니다. 또한 HotStuff는 블립 이후 백로그 처리로 인해 장시간 성능 저하가 발생하지만, Autobahn은 네트워크 안정화 즉시 전체 백로그를 단일 스텝으로 커밋합니다. 리더 실패나 부분적인 네트워크 분할 시에도 Autobahn은 데이터를 계속 전파하며, 합의 재개 후 누적된 프로포절들을 빠르게 커밋합니다. 이러한 성능 상의 이점은 낮은 레이턴시와 높은 처리량 및 장애 허용성을 결합하려는 블록체인 플랫폼에 Autobahn이 매우 좋은 선택지임을 보여줍니다. ### 추가 자료 더 많은 기술적 상세 내용은 다음 문서를 참고하세요: * [Autobahn: Seamless high speed BFT](https://arxiv.org/pdf/2401.10369) ## Bank ### 개요 Stable SDK의 `x/bank` 모듈은 기본적인 토큰 관리 기능만 제공합니다. 모든 토큰은 제한 없이 다른 계정으로 전송될 수 있으며, 사용자는 다른 계정에 토큰 전송 권한을 위임할 수 없습니다. 이러한 이유로, `bank` precompile 컨트랙트는 Stable SDK의 기존 `x/bank` 모듈 위에 추가적인 권한 부여 및 위임 기능을 제공합니다. ### 목차 1. **[개념](#concepts)** 2. **[설정](#configuration)** 3. **[메서드](#methods)** 4. **[이벤트](#events)** ### 개념 이 precompile 컨트랙트는 ERC20 표준 메서드를 제공합니다 - 전송을 위한 `transfer`와 `balanceOf`, 위임을 위한 `transferFrom`, `approve`, `allowance` 등이 있습니다. 이러한 메서드는 컨트랙트 주소 등록 없이 직접 호출할 수 있습니다. 그러나 `mint`와 `burn` 메서드는 `x/precompile` 모듈에 의해 등록된 컨트랙트 주소가 화이트리스트에 포함되어야 합니다. ```go func (p *Precompile) mint( ctx sdk.Context, contract *vm.Contract, denom string, method *abi.Method, stateDB vm.StateDB, args []interface{}, ) ([]byte, error) { // ... // mint method is only allowed for the registered caller contract if _, err := precompilecommon.CheckPermissions(ctx, p.precompileKeeper, contract.CallerAddress, CallerPermissions); err != nil { return nil, err } ``` 추가적인 검증 프로세스는 이 precompile 컨트랙트를 호출하는 토큰 컨트랙트가 승인되었음을 보장할 수 있습니다. 토큰 컨트랙트 주소와 해당 denom을 `x/precompile` 모듈의 화이트리스트에 등록하려면 거버넌스 제안이 필요합니다. ### 설정 컨트랙트 주소와 가스 비용은 사전 정의되어 있습니다. #### 컨트랙트 주소 * `0x0000000000000000000000000000000000001003` - STABLE (거버넌스 토큰) ### 메서드 #### `mint` 요청된 수량의 새로운 토큰을 발행하고 계정으로 전송합니다. 발행할 토큰의 수량은 0보다 커야 합니다. 토큰이 성공적으로 발행되고 계정으로 전송되면 `PrecompiledBankMint`가 발생합니다. 주의사항: * 거버넌스 토큰 발행은 금지되어 있습니다. * mint 메서드를 호출하는 컨트랙트는 x/precompile 모듈에 등록되어 있어야 합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ------------- | | to | address | 발행된 토큰을 받을 주소 | | amount | uint256 | 발행할 토큰의 수량 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ----------------------------- | | success | bool | 토큰이 성공적으로 발행되고 계정으로 전송되면 true | #### `burn` 계정에서 요청된 수량의 토큰을 소각합니다. 소각할 토큰의 수량은 0보다 커야 합니다. 토큰이 성공적으로 소각되면 `PrecompiledBankBurn`이 발생합니다. 주의사항: * 거버넌스 토큰 소각은 금지되어 있습니다. * mint 메서드를 호출하는 컨트랙트는 x/precompile 모듈에 등록되어 있어야 합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ---------- | | from | address | 토큰을 소각할 주소 | | amount | uint256 | 소각할 토큰의 수량 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 토큰이 성공적으로 소각되면 true | #### `transfer` 발신자로부터 수신자에게 요청된 수량의 토큰을 전송합니다. 토큰은 전송 가능하도록 설정되어 있어야 합니다. 전송할 토큰의 수량은 0보다 커야 합니다. 토큰이 성공적으로 전송되면 `PrecompiledBankTransfer`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ---------- | | to | address | 토큰을 받을 주소 | | amount | uint256 | 전송할 토큰의 수량 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 토큰이 성공적으로 전송되면 true | #### `transferFrom` 승인된 spender가 allowance 한도 내에서 소유자로부터 수신자에게 요청된 수량의 토큰을 전송합니다. 토큰은 전송 가능하도록 설정되어 있어야 합니다. 전송할 토큰의 수량은 0보다 크고 현재 allowance보다 작거나 같아야 합니다. 토큰이 성공적으로 전송되면 `PrecompiledBankTransfer`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ------- | ---------- | | from | address | 토큰을 전송할 주소 | | to | address | 토큰을 받을 주소 | | amount | uint256 | 전송할 토큰의 수량 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 토큰이 성공적으로 전송되면 true | #### `multiTransfer` 단일 계정에서 여러 계정으로 토큰을 전송합니다. 토큰은 전송 가능하도록 설정되어 있어야 합니다. 각 수신자에게 전송할 토큰의 수량은 0보다 커야 합니다. 토큰이 성공적으로 전송되면 각 수신자마다 `PrecompiledBankTransfer`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------ | ---------- | ------------------ | | to | address\[] | 전송된 토큰을 받을 주소들 | | amount | uint256\[] | 각 수신자에게 전송할 토큰의 수량 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ---------------------------- | | success | bool | 모든 수신자에게 토큰이 성공적으로 전송되면 true | #### `approve` spender가 소유자의 계정에서 토큰을 전송할 수 있도록 승인합니다. 승인할 토큰의 수량은 0보다 커야 합니다. 승인이 성공적으로 설정되면 `PrecompiledBankApproval`이 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ---------- | | spender | address | 승인할 주소 | | value | uint256 | 승인할 토큰의 수량 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 승인이 성공적으로 설정되면 true | #### `revoke` 소유자로부터 토큰을 전송할 수 있는 spender의 승인을 취소합니다. 승인이 성공적으로 취소되면 `PrecompiledBankRevoke`가 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ------ | | spender | address | 취소할 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ------------------- | | success | bool | 승인이 성공적으로 취소되면 true | #### `balanceOf` 계정의 토큰 잔액을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ------------- | | account | address | 토큰 잔액을 조회할 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ------- | --------- | | balance | uint256 | 계정의 토큰 수량 | #### `totalSupply` 토큰의 총 공급량을 반환합니다. ##### 입력 없음 ##### 출력 | 이름 | 타입 | 설명 | | ----------- | ------- | -------- | | totalSupply | uint256 | 토큰의 총 수량 | #### `allowance` spender가 owner로부터 여전히 인출할 수 있는 수량을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ------- | ------- | ----------- | | owner | address | 소유자의 주소 | | spender | address | spender의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------ | ------- | ---------- | | amount | uint256 | 승인된 토큰의 수량 | ### 이벤트 이 precompile 컨트랙트에서 발생하는 모든 이벤트는 `PrecompiledBank` 접두사가 붙습니다. 모호함을 피하기 위해, 이 precompile 컨트랙트를 호출하는 토큰 컨트랙트는 동일한 접두사를 가진 이벤트 이름 사용을 피해야 합니다. #### PrecompiledBankMint | 이름 | 타입 | 인덱싱 | 설명 | | ------ | ------- | --- | ------------- | | from | address | Y | 토큰을 발행한 주소 | | to | address | Y | 발행된 토큰을 받을 주소 | | amount | uint256 | N | 발행된 토큰의 수량 | #### PrecompiledBankBurn | 이름 | 타입 | 인덱싱 | 설명 | | ------ | ------- | --- | ---------------- | | from | address | Y | 토큰을 소각한 주소 | | to | address | Y | 이 메서드에서는 사용되지 않음 | | amount | uint256 | N | 소각된 토큰의 수량 | #### PrecompiledBankTransfer | 이름 | 타입 | 인덱싱 | 설명 | | ------ | ------- | --- | ------------- | | from | address | Y | 토큰을 전송한 주소 | | to | address | Y | 전송된 토큰을 받을 주소 | | amount | uint256 | N | 전송된 토큰의 수량 | #### PrecompiledBankApproval | 이름 | 타입 | 인덱싱 | 설명 | | ------- | ------- | --- | ---------- | | owner | address | Y | 토큰을 승인한 주소 | | spender | address | Y | 승인할 주소 | | value | uint256 | N | 승인된 토큰의 수량 | #### PrecompiledBankRevoke | 이름 | 타입 | 인덱싱 | 설명 | | ------- | ------- | --- | ---------- | | owner | address | Y | 토큰을 취소한 주소 | | spender | address | Y | 취소할 주소 | | value | uint256 | N | 승인된 토큰의 수량 | ## 브리지 보안과 DVN LayerZero 브리지의 보안은 한 체인에서 보낸 메시지가 다른 체인에서 발생했음을 확인하는 검증 계층만큼만 안전합니다. 그 계층이 바로 탈중앙화 검증자 네트워크(Decentralized Verifier Network, DVN)입니다. 이 페이지에서는 DVN이 무엇을 하는지, Stable이 자사 브리지에서 DVN을 어떻게 구성하는지, 그리고 단일 DVN의 손상이 왜 Stable을 위험에 빠뜨리지 않는지를 설명합니다. ### DVN의 작동 방식 LayerZero 메시지가 체인 A에서 체인 B로 이동할 때, 구성된 DVN 집합이 독립적으로 그 메시지가 진짜임을 입증하기 전까지 목적지 컨트랙트는 메시지를 실행하지 않습니다. 각 애플리케이션은 자체 구성을 선택합니다: * **필수 DVN.** 메시지가 수락되기 전에 모든 필수 DVN이 서명해야 합니다. * **N-of-M 임계값을 갖는 선택적 DVN.** 필수 집합 위에 선택적 풀을 추가할 수 있으며, 필수 서명에 더해 2-of-5와 같은 임계값을 충족해야 합니다. * **블록 확인 깊이.** DVN이 서명하기 전에 기다리는 소스 체인 확인 수입니다. 브리지의 안전성은 전적으로 이 구성에 달려 있습니다. 단일 DVN을 유일한 검증자로 두는 1/1 설정은 그 하나의 DVN 서명 키가 손상되면 공격자가 크로스체인 메시지를 위조할 수 있음을 의미합니다. 세 개의 독립적인 운영자에 걸친 3/3 구성은 세 운영자가 모두 동시에 손상되어야 합니다. 이 차이는 단일 도난 키로 브리지를 잃는 것과 한 운영자에 대한 표적 공격에서 살아남는 것의 차이입니다. ### Stable의 구성 Stable의 브리지는 세 개의 독립적인 운영자인 **LayerZero Labs**, **Canary**, **Horizen**과 함께 **3/3 필수 DVN** 구성을 실행합니다. 목적지 컨트랙트가 메시지를 실행하기 전에 세 운영자 모두가 모든 크로스체인 메시지에 서명해야 합니다. 임계값을 갖는 선택적 풀은 없으며, 필수 집합이 전체 검증 표면입니다. LayerZero 자체 키를 포함한 단일 서명 키의 손상은 이 태세에 아무런 영향을 미치지 못합니다. 메시지를 위조하려면 세 개의 독립적인 운영자 모두가 동시에 손상되어야 합니다. DVN 컨트랙트 주소는 [브리지: Stable의 DVN 운영자](/ko/reference/bridges#stable-s-dvn-operators)를 참조하세요. ### STABLE OFT 아키텍처 STABLE 토큰은 LayerZero의 Omnichain Fungible Token(OFT) 표준을 사용하여 다른 체인으로 브리지됩니다. 두 가지 컨트랙트 유형이 배포됩니다: * Stable의 **`StableOFTAdapter`**. 어댑터는 홈 체인에서 STABLE을 잠그고 STABLE이 크로스체인으로 전송될 때 LayerZero 메시지를 발생시킵니다. * 각 원격 체인의 **`StableOFTUpgradeable`**. 이 컨트랙트는 구성된 DVN이 메시지를 검증할 때 목적지에서 STABLE을 발행하고, 반환 경로에서 이를 소각하여 홈 체인 공급량이 정규(canonical) 상태로 유지되도록 합니다. 각 체인에 배포된 주소는 [브리지: STABLE OFT 컨트랙트](/ko/reference/bridges#stable-oft-contracts)를 참조하세요. ### 운영상의 의존성 Stable 자체의 브리지 보안은 상위 프로토콜과 독립적이지만, 파트너 프로토콜이 자체 브리지를 일시 중지할 때 Stable을 통한 크로스체인 흐름은 여전히 일시 중지될 수 있습니다. 예를 들어 USDT0가 크로스체인 발행과 소각을 일시 중지하면, USDT0가 재개될 때까지 USDT0는 Stable로 또는 Stable에서 이동할 수 없습니다. Stable 내의 자금은 계속 자유롭게 이동하며, 특정 크로스체인 작업만 사용할 수 없습니다. 파트너 브리지를 통해 라우팅되는 애플리케이션 표면은 사용자가 그 구분을 이해할 수 있도록 이를 명확하게 전달해야 합니다. 사용자의 자금은 위험에 처한 것이 아니라, 특정 크로스체인 경로만 일시적으로 사용할 수 없는 것입니다. ### 다음 권장 사항 * [**USDT0를 Stable로 브리징하기**](/ko/explanation/usdt0-bridging) — USDT0가 OFT Mesh와 Legacy Mesh를 통해 Stable에 도달하는 방법을 확인하세요. * [**브리지 제공자와 주소**](/ko/reference/bridges) — 컨트랙트 주소, DVN 운영자, 지원되는 브리지 제공자를 참조하세요. * [**LayerZero DVN 문서**](https://docs.layerzero.network/v2/concepts/protocol/security-stack-dvns) — 필수 및 선택적 DVN 검증에 대한 LayerZero의 사양을 읽어보세요. ## 개요 Stable이 처음이신가요? 먼저 [빠른 시작](/ko/tutorial/quick-start)을 실행하세요. 5분이면 테스트넷 트랜잭션을 전송하므로, 이 탭의 나머지 내용을 연결할 무언가가 생깁니다. ### 살펴보기 * [**계정**](/ko/explanation/accounts-overview) — 지갑을 생성하고, EIP-7702로 EOA를 위임하며, 사용자와 에이전트를 위한 세션 키 범위를 지정하세요. * [**결제**](/ko/explanation/payments-overview) — USDT0를 전송하고, P2P 및 구독 흐름을 구축하며, ERC-3009로 인보이스를 정산하고, x402로 API 가격을 책정하세요. * [**컨트랙트**](/ko/explanation/contracts-overview) — Solidity 컨트랙트를 배포, 검증, 인덱싱하고, Bank / Distribution / Staking 프리컴파일을 호출하세요. * [**AI와 에이전트**](/ko/explanation/agent-settlement) — MCP 서버를 AI 클라이언트에 연결하고, 에이전트가 프롬프트를 통해 호출할 수 있는 x402 유료 도구를 노출하세요. ### 여기서 시작하기 * [**빠른 시작**](/ko/tutorial/quick-start) — 테스트넷에 연결하고, 지갑에 자금을 충전하며, 첫 USDT0 트랜잭션을 전송하세요. * [**첫 USDT0 전송하기**](/ko/tutorial/send-usdt0) — 동일한 잔액에서 네이티브 및 ERC-20 전송을 TypeScript 예제와 함께 수행하세요. * [**스마트 컨트랙트 배포하기**](/ko/tutorial/smart-contract) — Foundry를 스캐폴딩하고, Stable을 구성하며, Counter 컨트랙트를 배포하세요. * [**Stable SDK**](/ko/explanation/sdk-overview) — Stable에서 transfer, bridge, swap을 위한 타입이 지정된 TypeScript 클라이언트를 사용하세요. ## 기밀 전송 기업들 사이에서 블록체인 채택이 특히 스테이블코인 분야에서 가속화됨에 따라, 트랜잭션 프라이버시에 대한 수요도 점점 증가하고 있습니다. 기업들은 종종 결제 금액과 같은 민감한 데이터를 보호하기 위해 금융 운영에서 기밀성을 요구합니다. 이러한 요구를 해결하기 위해 Stable은 프라이버시 요구와 규제 준수라는 두 가지 필수 요소의 균형을 맞춘 기밀 전송 메커니즘을 개발 중입니다. Stable은 영지식(ZK) 암호 기술을 활용하여 트랜잭션 금액을 온체인에 공개하지 않고도 토큰을 전송할 수 있도록 하는 기밀 전송 레이어를 구축하고 있습니다. 전송 금액은 비공개로 유지되는 반면, 송신자와 수신자의 주소는 공개된 상태로 유지되어 금융 규제 및 감사 가능성을 확보합니다. 보호된 트랜잭션 금액은 해당 거래 당사자 및 인가된 규제 감사인만 접근할 수 있어, 프라이버시 확보가 법적 투명성을 희생하지 않도록 보장합니다. ## 합의 ### StableBFT를 활용한 PoS 합의 Stable Blockchain은 CometBFT를 기반으로 구축된 맞춤형 PoS 합의 프로토콜인 **StableBFT**를 활용하여 높은 처리량, 낮은 지연 시간, 그리고 강력한 신뢰성을 제공합니다. StableBFT는 결정론적 완결성(블록은 포함되는 즉시 최종 확정되며 포크가 없음)과 검증자의 1/3까지 장애가 발생하거나 악의적으로 행동하더라도 견딜 수 있는 비잔틴 장애 허용성을 제공합니다. 합의 성능을 한층 더 최적화하기 위해 Stable은 가까운 미래에 다음과 같은 개선 사항을 구현할 계획입니다: * **트랜잭션과 합의 가십의 분리(Decoupled Transaction and Consensus Gossiping)**: 트랜잭션 가십 계층을 합의 가십 계층과 분리함으로써, 트랜잭션 측의 네트워크 혼잡이 합의 통신을 방해하는 것을 방지합니다. * **블록 제안자에게 트랜잭션 직접 브로드캐스트(Direct Transaction Broadcasting to the Block Proposer)**: 현재 모델에서는 트랜잭션이 노드 간 피어 투 피어 가십을 통해 전파되어 네트워크 전반에 높은 가십 트래픽을 발생시킵니다. Stable은 트랜잭션이 블록 제안자에게 직접 브로드캐스트되도록 함으로써 효율성을 개선하고자 합니다. ### 향후 로드맵: DAG 기반 합의 합의를 크게 가속화하기 위해, Stable은 최대 5배의 속도 향상을 제공할 수 있는 DAG 기반 설계로 프로토콜을 업그레이드할 계획입니다. PBFT나 HotStuff 같은 전통적인 뷰 기반 BFT 프로토콜은 안정적인 네트워크 조건에서 낮은 지연 시간에 최적화되어 있습니다. 그러나 이러한 프로토콜은 장애 상황에서 성능이 크게 저하되며, 일시적인 결함 이후 종종 긴 복구 지연을 겪습니다. Narwhal과 Tusk 같은 1세대 DAG 기반 엔진은 데이터 전파를 합의 순서화로부터 분리함으로써 단일 제안자 병목 현상을 제거하고 네트워크 불안정 상황에서의 견고성도 향상시킬 수 있음을 보여줍니다. 그러나 이들의 아키텍처는 기존의 높이 기반 블록 시맨틱과 멤풀 설계에서 벗어나기 때문에 CometBFT와 같은 시스템과 직접적으로 호환되지는 않습니다. [Autobahn](/ko/explanation/autobahn)은 Stable의 합의 계층과 더 자연스럽게 통합되는 PBFT-on-DAG 아키텍처를 제공하며, 정상적인 조건에서 낮은 지연 시간을 제공하는 동시에 네트워크 결함 발생 시 빠른 복구를 제공합니다. Stable 팀은 Autobahn 논문 저자들과 긴밀한 관계를 유지하고 있으며, Autobahn의 아키텍처를 활용하여 StableBFT의 성능을 극대화할 것입니다. Autobahn 위에 구축된 StableBFT는 다음을 가능하게 합니다: * 단일 리더 제한을 제거하여 병렬 제안 처리. * 데이터 전파를 최종 순서화로부터 분리하여 더 빠른 완결성. * 견고한 BFT 메커니즘을 통한 네트워크 장애에 대한 향상된 복원력. 이 고급 합의 설계는 내부 개념 증명(proof-of-concept)을 기반으로 훨씬 더 높은 처리량을 지원하며, 통제된 환경에서 200,000 TPS 이상(합의 전용)을 입증했습니다. ### 다음 권장 사항 * [**Autobahn**](/ko/explanation/autobahn) — StableBFT의 DAG 기반 업그레이드 경로를 뒷받침하는 프로토콜 논문을 읽어보세요. * [**실행**](/ko/explanation/execution) — 블록이 합의에서 병렬 실행으로 어떻게 이동하는지 확인하세요. * [**완결성**](/ko/explanation/finality) — RPC를 기반으로 구축할 때 Stable의 단일 슬롯 완결성을 적용하세요. ## 컨트랙트 가이드 컨트랙트 탭 아래의 모든 가이드, 개념, 레퍼런스를 작업 목적별로 분류했습니다. ### 컨트랙트 빌드 및 배포 * [**배포**](/ko/tutorial/smart-contract) — Foundry 프로젝트를 스캐폴딩하고 Counter를 Stable 테스트넷에 배포합니다. * [**검증**](/ko/how-to/verify-contract) — Stablescan에 소스를 업로드하여 사용자가 컨트랙트를 읽고 호출할 수 있도록 합니다. * [**이벤트 인덱싱**](/ko/how-to/index-contract) — ethers.js로 실시간 이벤트 스트림을 구축하고 과거 데이터를 백필합니다. ### 시스템 모듈 호출 * [**시스템 모듈 사용**](/ko/how-to/use-system-modules) — Solidity 또는 ethers.js에서 Bank, Distribution, Staking 프리컴파일을 호출합니다. * [**언본딩 완료 추적**](/ko/how-to/track-unbonding) — StableSystem 프리컴파일을 통해 발생하는 UnbondingCompleted 이벤트를 구독합니다. ### 레퍼런스 * [**시스템 모듈 레퍼런스**](/ko/reference/system-modules-api-overview) — 프리컴파일 주소 및 모듈별 ABI 포인터. * [**JSON-RPC API**](/ko/reference/json-rpc-api) — 지원되는 `eth_*`, `net_*`, `web3_*`, `debug_*` 메서드. ### 기초 개념 * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 이중 역할 잔액, 조정 이벤트, 컨트랙트 설계 규칙. * [**이더리움과의 차이점**](/ko/explanation/ethereum-comparison) — 가스 토큰, 최종성, 우선순위 팁, EVM 호환성. ## Stable의 컨트랙트 Stable은 완전히 EVM 호환됩니다. Solidity, Vyper, Hardhat, Foundry, ethers.js, viem이 변경 없이 작동합니다. 도구를 Stable의 RPC로 가리키기만 하면 기존 컨트랙트가 그대로 배포됩니다. 표준 EVM 위에, Stable은 프로토콜 수준 모듈(Bank, Distribution, Staking)을 고정 주소의 프리컴파일된 컨트랙트로 노출하므로, Solidity에서 스테이킹과 보상 분배를 다시 구현하지 않고도 호출할 수 있습니다. ### 무엇을 만들 수 있는가 * 어떤 EVM 도구체인으로든 **표준 애플리케이션 컨트랙트**(ERC-20, ERC-721, 에스크로, AMM)를 만들 수 있습니다. * ethers.js를 통한 실시간 이벤트 스트림과 함께 Stablescan에서 **검증되고 인덱싱된 컨트랙트**. * Solidity에서 Bank / Distribution / Staking 프리컴파일을 호출하는 **프로토콜 통합 컨트랙트**. * 표준 `eth_getLogs`를 통해 프로토콜이 발생시킨 이벤트(예: 언본딩 완료)를 감시하는 **시스템 트랜잭션 리스너**. ### Stable이 다른 점 * **USDT0가 가스 토큰입니다.** `maxPriorityFeePerGas`는 `0`이어야 합니다. 네이티브 전송의 `value` 필드는 ETH가 아닌 USDT0를 전달합니다. [USDT0를 가스로 사용하기](/ko/how-to/work-with-usdt-gas)를 참고하세요. * **USDT0는 이중 역할을 합니다.** 네이티브 USDT0를 보유한 컨트랙트는 ERC-20 `transferFrom`이나 `permit`에 의해 잔액이 변경될 수 있으므로 — 절대 네이티브 잔액을 `uint256`에 미러링하지 마세요. [Stable에서의 USDT0 동작](/ko/explanation/usdt0-behavior)을 참고하세요. * **프리컴파일 주소는 테스트넷과 메인넷에서 동일하게 고정되어 있습니다.** 컨트랙트에 상수로 박아 넣으세요. ### 여기서 시작하세요 * [**배포**](/ko/tutorial/smart-contract) — Foundry를 스캐폴딩하고, Stable을 구성하고, Counter를 배포합니다. * [**검증**](/ko/how-to/verify-contract) — forge verify-contract로 Stablescan에 소스를 업로드합니다. * [**인덱싱**](/ko/how-to/index-contract) — ethers.js로 이벤트를 구독하고 과거 로그를 백필합니다. ### 다음 권장 사항 * [**컨트랙트 가이드 색인**](/ko/explanation/contracts-guides) — 컨트랙트 가이드, 프리컴파일 참조, 시스템 모듈 ABI의 전체 목록. * [**시스템 모듈 사용하기**](/ko/how-to/use-system-modules) — Solidity와 ethers.js에서 Bank / Distribution / Staking을 호출합니다. * [**JSON-RPC 참조**](/ko/reference/json-rpc-api) — Stable이 지원하는 `eth_*` 및 `debug_*` 메서드. ## 핵심 개념 빌드를 시작하기에는 네 가지 개념으로 충분합니다. 각 섹션은 개념을 정의하고, 보여주며, 전체 레퍼런스로 연결합니다. ### 가스로서의 USDT0 여러분은 이미 보유하고 거래 중인 동일한 자산인 USDT0로 트랜잭션 수수료를 지불합니다. 자금을 조달하거나 관리해야 할 두 번째 토큰이 없습니다. USDT0는 네이티브 가스 자산(18 decimals, `address(x).balance`로 조회)이자 ERC-20 토큰(6 decimals, `USDT0.balanceOf(x)`로 조회)입니다. 두 인터페이스 모두 동일한 기본 잔액에서 작동하며, 프로토콜이 12자리 정밀도 차이를 자동으로 조정합니다. ```solidity // Both read the same balance: uint256 native = address(user).balance; // 18 decimals uint256 erc20 = IERC20(USDT0).balanceOf(user); // 6 decimals ``` :::warning 잔액 조정은 예약 주소 `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`에서 추가 `Transfer` 이벤트를 발생시킵니다. `Transfer` 이벤트를 재생하는 인덱서는 이 주소로 또는 이 주소로부터의 전송을 필터링해야 하며, 그렇지 않으면 잔액을 조용히 이중 계산하게 됩니다. ::: 더 읽어보기: [가스로서의 USDT0](/ko/explanation/usdt-as-gas-token) · [Stable에서의 USDT0 동작](/ko/explanation/usdt0-behavior). ### 보장된 블록 공간 Stable은 각 블록 용량의 일부를 사전 할당된 엔터프라이즈 워크로드를 위해 예약합니다. 예약된 트래픽은 일반 트래픽이 혼잡할 때도 예측 가능한 지연 시간과 비용으로 정산되며, 수수료 시장에서 경쟁하지 않습니다. 이 동작은 호출자 수준에서 투명합니다. 여러분은 평소대로 트랜잭션을 제출하고, 할당은 등록된 계정에 대해 프로토콜 수준에서 적용됩니다. 더 읽어보기: [보장된 블록 공간](/ko/explanation/guaranteed-blockspace). ### USDT 전송 집계기 대용량 USDT0 전송은 MapReduce에서 영감을 받은 파이프라인을 사용하여 병렬로 배치 처리되고 검증됩니다. 계정별 실패는 격리되므로, 하나의 잘못된 전송이 배치를 중단시키지 않습니다. 호출자 측 전송 API는 변경되지 않습니다. 여러분은 평소대로 전송을 제출하고 코드 변경 없이 처리량을 얻습니다. 더 읽어보기: [USDT 전송 집계기](/ko/explanation/usdt-transfer-aggregator). ### EVM 호환성 표준 EVM 도구가 변경 없이 작동합니다. EVM 수준에서 세 가지 동작이 Ethereum과 다릅니다(위에서 다룬 가스로서의 USDT0가 네 번째입니다). **단일 슬롯 완결성.** 트랜잭션은 블록에 포함되면 즉시 완결됩니다. 블록은 대략 0.7초마다 생성됩니다. **우선순위 팁 없음.** `maxPriorityFeePerGas`는 항상 무시됩니다. 유효 가스 가격은 프로토콜이 설정한 기본 수수료입니다. ```typescript import { ethers } from "ethers"; const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: ethers.parseEther("0.1"), maxFeePerGas: baseFee * 2n, // 2x base fee as safety margin maxPriorityFeePerGas: 0n, // always 0 on Stable }); await tx.wait(); console.log("Included at gas price:", tx.gasPrice?.toString()); ``` ```text Included at gas price: 1000000000 ``` **이중 역할 USDT0, 포팅 위험.** Ethereum에서 포팅된 컨트랙트는 네이티브 잔액을 미러링해서는 안 되며, `address(0)` 전송을 거부해야 하고, 주소 재사용 감지에 `EXTCODEHASH`를 의존해서는 안 됩니다. :::warning 내부 변수에서 네이티브 잔액을 미러링하는 컨트랙트를 포팅하는 것은 Stable에서 안전하지 않습니다. 외부 `USDT0.transferFrom` 호출은 어떠한 컨트랙트 코드도 실행하지 않고 컨트랙트의 네이티브 잔액을 고갈시킬 수 있습니다. 항상 전송 시점에 `address(this).balance`로 지급 능력을 확인하세요. ::: 더 읽어보기: [Ethereum과의 차이점](/ko/explanation/ethereum-comparison) · [Stable의 컨트랙트](/ko/explanation/contracts-overview) · [USDT0 마이그레이션 체크리스트](/ko/explanation/usdt0-behavior). ### 기밀 전송 (예정) Stable은 권한이 있는 당사자에게는 감사 가능성을 유지하면서 금액을 숨기는 영지식 전송 기능을 계획하고 있습니다. 아직 출시되지 않았습니다. 더 읽어보기: [기밀 전송](/ko/explanation/confidential-transfer). ### 다음 추천 * [**빠른 시작**](/ko/tutorial/quick-start) — 테스트넷에 연결하고 첫 트랜잭션을 전송합니다. * [**USDT0 동작**](/ko/explanation/usdt0-behavior) — 이중 역할의 함정에 빠지지 않고 컨트랙트를 Stable로 포팅합니다. * [**가스 가격 책정**](/ko/reference/gas-pricing-api) — Stable의 수수료 모델에 맞게 트랜잭션을 올바르게 구성합니다. * [**프로덕션 준비**](/ko/how-to/production-readiness) — 메인넷에 출시하기 전에 통합을 검증합니다. ## 개요 ### 풀스택 코어 최적화 Blockchain Transaction Lifecycle 제출부터 결과 완결까지 블록체인 트랜잭션의 라이프사이클은, 강하게 연결된 여러 단계로 구성되어 있습니다. 트랜잭션은 **RPC** 인터페이스를 통해 제출되어, **멤풀**에 추가되고, 블록에 포함되며, **합의**를 통해 검증되고, **상태 머신**에 의해 실행되며, **데이터베이스**에 최종 저장됩니다. 전체 파이프라인을 모두 완료한 이후에야 사용자는 확정된 결과를 받을 수 있습니다. 단 하나의 단계만 개선하는 것으로는 충분하지 않습니다. 파이프라인 내 비효율적인 부분이 하나라도 존재한다면, 이는 전체 시스템의 성능에 악영향을 줄 수 있습니다. Stable이 블록체인 스택 전체를 최적화하는 데에 집중하는 이유입니다. 전체 네트워크에 걸쳐 Stable이 합의, 실행, 데이터베이스, RPC를 포함한 전체 스택 내 각 레이어를 어떻게 최적화하여 신뢰성 있고 고성능의 트랜잭션 처리를 가능하게 하는지 알고 싶으시다면, 다음 페이지들을 읽어보세요. ## Distribution ### 개요 `distribution` precompile 컨트랙트는 Stable SDK의 `x/distribution` 모듈 기능을 EVM 환경에서 사용할 수 있도록 브리지 역할을 합니다. ### 목차 1. **[개념](#개념)** 2. **[구성](#구성)** 3. **[메서드](#메서드)** 4. **[이벤트](#이벤트)** ### 개념 `distribution` precompile 컨트랙트에서는 위임자 또는 예치자가 호출자인지 확인하는 추가 검사가 수행됩니다. ### 구성 컨트랙트 주소와 가스 비용은 사전 정의되어 있습니다. #### 컨트랙트 주소 * `0x0000000000000000000000000000000000000801` ### 메서드 #### `setWithdrawAddress` 위임자가 검증자에게 위임한 토큰에 대한 rewards를 받을 주소를 설정합니다. 때로는 위임자가 자기 위임(self-delegated)일 때 검증자 주소가 위임자로 사용됩니다. `SetWithdrawAddress`는 출금 주소가 성공적으로 설정되었을 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ----------------- | ------- | --------------------- | | delegatorAddress | address | 위임자의 주소 | | withdrawerAddress | address | 위임에 대한 rewards를 받을 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---- | ---------------------- | | success | bool | 출금 주소가 성공적으로 설정되면 true | #### `withdrawDelegatorRewards` 검증자로부터 위임자가 받을 rewards를 출금합니다. 검증자가 위임자에게 지급하는 모든 유형의 토큰이 단일 트랜잭션으로 출금됩니다. `WithdrawDelegatorRewards`는 rewards가 성공적으로 출금되었을 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | delegatorAddress | address | 위임자의 주소 | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------ | ------- | ----------------------- | | amount | Coin\[] | 위임자가 받을 다양한 토큰의 rewards | `Coin`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------ | ------- | ------------- | | denom | string | reward의 denom | | amount | uint256 | reward의 수량 | #### `withdrawValidatorCommission` 검증자의 수수료를 출금합니다. 검증자가 수수료로 받는 모든 유형의 토큰이 단일 트랜잭션으로 출금됩니다. `WithdrawValidatorCommission`은 수수료가 성공적으로 출금되었을 때 발생합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------ | ------- | ------------------- | | amount | Coin\[] | 검증자가 받을 다양한 토큰의 수수료 | #### `validatorDistributionInfo` 검증자가 받을 reward를 나타내는 분배 정보를 반환합니다. 검증자는 자신의 주소로 토큰을 위임하여 자기 결합(self-bonded)이라고 하는 위임자 역할을 할 수 있습니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ---------------- | ------------------------- | ---------- | | distributionInfo | ValidatorDistributionInfo | 검증자의 분배 정보 | `ValidatorDistributionInfo`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------------- | ---------- | ------------- | | operatorAddress | address | 검증자의 운영자 주소 | | selfBondRewards | DecCoin\[] | 검증자의 자기 결합 수량 | | commission | DecCoin\[] | 검증자의 수수료 | `DecCoin`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------- | ------- | ------------- | | denom | string | reward의 denom | | amount | uint256 | reward의 수량 | | precision | uint8 | reward의 정밀도 | #### `validatorOutstandingRewards` 검증자의 미지급 rewards를 반환합니다. 미지급 rewards는 검증자의 수수료와 자기 결합 rewards, 그리고 위임자들의 총 rewards로 구성된 총 reward 금액을 나타냅니다. 검증자 A가 있고 위임자 B, C, D가 A에게 위임하는 경우, 검증자의 미지급 rewards는 A의 수수료와 자기 결합 rewards + B, C, D의 rewards의 합계입니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---------- | ---------------- | | rewards | DecCoin\[] | 검증자의 미지급 rewards | #### `validatorCommission` 검증자의 수수료를 반환합니다. 이 메서드는 `withdrawValidatorCommission` 메서드를 호출하기 전에 검증자의 수수료를 조회하는 데 사용됩니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ------- | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ---------- | ---------- | -------- | | commission | DecCoin\[] | 검증자의 수수료 | #### `validatorSlashes` 시작 높이와 종료 높이 사이에 검증자의 슬래시 이력을 반환합니다. 슬래싱은 검증자가 악의적으로 행동하거나 이중 서명, 잘못된 행동, 체인 규칙을 따르지 않는 등의 네트워크 규칙을 위반했을 때 부과되는 벌금입니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | --------- | | validatorAddress | address | 검증자의 주소 | | startingHeight | uint64 | 시작 높이 | | endingHeight | uint64 | 종료 높이 | | pageRequest | PageReq | 페이지네이션 요청 | `PageReq`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ---------- | ------ | ------------------ | | key | bytes | 페이지네이션의 키 | | offset | uint64 | 페이지네이션의 오프셋 | | limit | uint64 | 페이지네이션의 제한 | | countTotal | bool | 총 페이지 수를 계산할지 여부 | | reverse | bool | 페이지네이션을 역순으로 할지 여부 | ##### 출력 | 이름 | 타입 | 설명 | | ---------- | ---------------------- | --------- | | slashes | ValidatorSlashEvent\[] | 검증자의 슬래시 | | pagination | PageResp | 페이지네이션 응답 | `ValidatorSlashEvent`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------------- | ------ | ------- | | validatorPeriod | uint64 | 검증자의 기간 | | fraction | Dec | 슬래시의 비율 | `Dec`은 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | --------- | ------ | -------- | | value | uint64 | Dec의 값 | | precision | uint8 | Dec의 정밀도 | `PageResp`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ------- | ------ | ------------ | | nextKey | bytes | 페이지네이션의 다음 키 | | total | uint64 | 총 페이지 수 | #### `delegationRewards` 위임자가 검증자로부터 받는 rewards를 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | | validatorAddress | address | 검증자의 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---------- | ---------------------- | | rewards | DecCoin\[] | 위임자가 검증자로부터 받는 rewards | #### `delegationTotalRewards` 위임자가 모든 검증자로부터 받는 총 rewards를 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ------- | ---------------------------- | --------------------------- | | rewards | DelegationDelegatorReward\[] | 위임자가 모든 검증자로부터 받는 총 rewards | | total | DecCoin\[] | rewards의 총 수량 | `DelegationDelegatorReward`는 다음 필드를 가진 구조체입니다: | 이름 | 타입 | 설명 | | ---------------- | ---------- | ---------------------- | | validatorAddress | address | 검증자의 주소 | | reward | DecCoin\[] | 위임자가 검증자로부터 받는 rewards | #### `delegatorValidators` 위임자가 결합된 검증자들을 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | ##### 출력 | 이름 | 타입 | 설명 | | ---------- | --------- | ------------- | | validators | string\[] | 위임자가 결합된 검증자들 | #### `delegatorWithdrawAddress` `setWithdrawAddress` 메서드로 설정한 위임 rewards를 받을 주소를 반환합니다. ##### 입력 | 이름 | 타입 | 설명 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 위임자의 hex 주소 | ##### 출력 | 이름 | 타입 | 설명 | | --------------- | ------- | ----------------- | | withdrawAddress | address | 위임 rewards를 받을 주소 | ### 이벤트 #### SetWithdrawAddress | 이름 | 타입 | 인덱스 | 설명 | | --------------- | ------- | --- | ----------------- | | caller | address | Y | 호출자(위임자)의 주소 | | withdrawAddress | address | N | 위임 rewards를 받을 주소 | #### WithdrawDelegatorRewards | 이름 | 타입 | 인덱스 | 설명 | | ---------------- | ------- | --- | ---------- | | delegatorAddress | address | Y | 위임자의 주소 | | validatorAddress | address | Y | 검증자의 주소 | | amount | uint256 | N | reward의 수량 | #### WithdrawValidatorCommission | 이름 | 타입 | 인덱스 | 설명 | | ---------------- | ------- | --- | --------- | | validatorAddress | address | Y | 검증자의 주소 | | commission | uint256 | N | 수수료의 총 수량 | ## EIP-7702 Stable은 **EIP-7702**를 지원하여 EOA(외부 소유 계정)가 임시로 스마트 계정처럼 작동할 수 있는 통합 계정 모델을 도입합니다. **주요 동작:** * 사용자는 기존 키로 트랜잭션에 서명합니다 * 영구적인 계정 업그레이드 없이 컨트랙트와 같은 로직을 실행에 통합할 수 있습니다 * 결제 및 수탁을 위한 안전한 승인 흐름을 가능하게 합니다 * USDT를 사용하는 지갑과 가맹점의 UX를 개선합니다 개발자에게 미치는 영향: * 사용자는 새로운 컨트랙트를 배포하지 않고도 컨트랙트급 기능을 획득하여 온보딩 마찰을 줄이고 수탁을 단순화합니다. ## 서명된 인가로 정산하기 ERC-3009는 토큰 보유자가 메시지에 서명하여 전송을 인가할 수 있게 합니다. 그러면 누구든지 그 서명된 인가를 제출하여 온체인에서 전송을 실행할 수 있습니다. 송신자는 컨트랙트를 직접 호출할 필요가 전혀 없습니다. 이것은 Stable에서 [x402](/ko/explanation/x402) 결제를 뒷받침하는 정산 메커니즘입니다. ### 어떤 문제를 해결하나요? #### 허용량(allowance) 문제 제3자 전송을 위한 전통적인 ERC-20 패턴은 `approve` + `transferFrom`입니다. 송신자는 먼저 `approve`를 호출하여 지출 허용량을 부여한 다음, 제3자가 `transferFrom`을 호출하여 자금을 옮깁니다. 여기에는 잘 알려진 문제들이 있습니다: * **두 개의 트랜잭션 필요**: 송신자는 어떤 전송이 일어나기 전에 먼저 온체인 `approve` 트랜잭션을 보내야 합니다. 이는 가스 비용이 들고 지연이 추가됩니다. * **무한 허용량 위험**: 반복적인 승인 트랜잭션을 피하기 위해 많은 애플리케이션이 무제한 지출 권한을 요청하며, 이는 심각한 보안 위험을 만듭니다. ERC-3009는 다른 접근 방식을 취합니다. 허용량을 부여하는 대신, 송신자는 특정 전송에 대한 일회성 인가에 서명합니다. 별도의 승인 단계도, 남아있는 지출 권한도 없습니다. #### 순차 nonce 문제 ERC-2612(`permit`) 역시 서명된 인가를 가능하게 하지만, 순차 nonce를 사용합니다. 여러 permit는 순서 의존성을 가집니다: nonce 5가 소비되지 않으면, nonce 6은 절대 실행될 수 없습니다. ERC-3009는 이를 **고유 nonce**로 해결합니다. 각 인가는 순차 카운터 대신 32바이트 값을 사용합니다. 여러 인가가 서로 의존하지 않고, 어떤 순서로든 독립적으로 생성되고 제출될 수 있습니다. #### 비교 | **속성** | **ERC-20** (`approve`) | **ERC-2612** (`permit`) | **ERC-3009** | | :-------- | :----------------------------- | :---------------------- | :------------------------------ | | 온체인 단계 | 2 (`approve` + `transferFrom`) | 1 (`transferFrom`) | 1 (`transferWithAuthorization`) | | 허용량 모델 사용 | 필요 (온체인 tx) | 예 (`permit`을 통해 허용량 설정) | 불필요 (서명) | | Nonce 모델 | 순차 | 순차 | 고유 | | 동시 인가 | 아니오 | 아니오 | 예 | ### 작동 방식 #### transferWithAuthorization 송신자는 전송 세부 정보를 담은 EIP-712 타입 데이터 메시지에 서명합니다. 그러면 누구든지 그 서명된 메시지로 토큰 컨트랙트의 `transferWithAuthorization`를 호출할 수 있습니다. 컨트랙트는 서명을 검증하고, 유효 기간을 확인하고, 전송을 실행하고, nonce를 사용됨으로 표시합니다. 서명된 인가에는 다음이 포함됩니다: * `from`: 송신자(서명자)의 주소 * `to`: 수신자의 주소 * `value`: 전송 금액 * `validAfter`: 이 인가가 실행될 수 있는 가장 이른 시점 (Unix 타임스탬프) * `validBefore`: 이 인가가 실행될 수 있는 가장 늦은 시점 (Unix 타임스탬프) * `nonce`: 고유성을 보장하는 32바이트 값 시간 기간(`validAfter`/`validBefore`)은 송신자에게 전송이 언제 일어날 수 있는지에 대한 정밀한 제어권을 줍니다. 인가는 미래로 예약하거나, 마감 기한을 정하거나, 둘 다 할 수 있습니다. 제출 전에 기간이 만료되면 인가는 무효가 되고 자금은 송신자에게 남습니다. #### receiveWithAuthorization 이 함수는 `transferWithAuthorization`와 동일하게 작동하지만, 한 가지 추가 검사가 있습니다: **호출자는 반드시 수신자여야 합니다**. 이는 제3자가 대기 중인 인가를 관찰하고 먼저 제출하여 트랜잭션 순서를 조작하는 프런트러닝 공격을 방지합니다. 이는 수신자(가맹점 또는 서비스 제공자)가 정산을 개시하는 주체여야 하는 결제 시나리오에서 유용합니다. #### cancelAuthorization 송신자는 사용되지 않은 인가가 실행되기 전에 이를 취소할 수 있습니다. 송신자가 EIP-712 취소 메시지에 서명하면, 컨트랙트는 전송을 실행하지 않고 nonce를 사용됨으로 표시합니다. 원래의 인가는 더 이상 제출될 수 없습니다. ### 내장된 안전 속성 * **일회성 사용**: 각 고유 nonce는 한 번만 사용할 수 있습니다. 동일한 서명된 인가를 다시 제출하면 되돌려집니다(revert). * **시간 제한**: `validAfter`/`validBefore` 기간은 인가가 무기한 유효하게 남지 않도록 보장합니다. * **자기 완결성**: 하나의 서명은 하나의 특정 수신자에게 하나의 특정 금액에 대한 하나의 특정 전송을 인가합니다. 남아있는 권한이 없습니다. * **비수탁(Non-custodial)**: 제출자는 송신자의 자금을 절대 보유하지 않습니다. 전송은 컨트랙트 내에서 송신자로부터 수신자에게 직접 이동합니다. ### Stable에서의 ERC-3009 Stable의 USDT0는 ERC-3009를 기본적으로 구현합니다. 어떤 애플리케이션이든 추가 컨트랙트나 릴레이 인프라를 배포하지 않고도 `transferWithAuthorization`를 사용할 수 있습니다. #### 단일 자산 정산 이더리움에서는 ERC-3009를 사용하더라도 제출자가 `transferWithAuthorization`를 호출하는 가스를 지불하기 위해 ETH가 필요합니다. 전송 자체는 USDT이지만, 실행은 별도의 네이티브 자산에 의존합니다. Stable에서는 USDT0가 결제 토큰이자 가스 토큰 역할을 동시에 합니다. 인가부터 온체인 정산까지 전체 결제 라이프사이클이 단일 스테이블코인 위에서 실행됩니다. 어떤 단계에서도 별도의 네이티브 자산이 필요하지 않습니다. 이 속성이 바로 Stable에서의 ERC-3009를 상위 수준 결제 프로토콜을 위한 강력한 기반으로 만드는 요소입니다. [x402](/ko/explanation/x402)는 이를 직접 활용하여, 표준 HTTP 통신 내에서 ERC-3009를 온체인 정산 메커니즘으로 사용합니다. ### 핵심 요약 * ERC-3009는 토큰 보유자가 메시지에 서명하여 전송을 인가할 수 있게 합니다. 누구든지 그 서명된 인가를 제출하여 전송을 실행할 수 있습니다. * 이는 ERC-20 허용량 모델을 일회성 사용, 자기 완결적 인가로 대체합니다. `approve` 단계도, 남아있는 권한도, 이중 지출 위험도 없습니다. * 고유 nonce는 여러 인가가 어떤 순서로든 동시에 생성되고 제출될 수 있게 합니다. * Stable의 USDT0는 ERC-3009를 기본적으로 지원하며, USDT0만으로 정산을 완료할 수 있기 때문에 x402를 위한 실용적인 기반을 제공합니다. **함께 보기:** * [가스로서의 USDT](/ko/explanation/usdt-as-gas-token) * [Stable에서의 USDT0 동작](/ko/explanation/usdt0-behavior) * [x402 (HTTP 네이티브 결제)](/ko/explanation/x402) ## Ethereum 비교 Stable은 완전히 EVM과 호환되므로 대부분의 Ethereum 도구, 라이브러리, 컨트랙트 패턴이 수정 없이 작동합니다. 아래 섹션에서는 Ethereum에서 Stable로 옮길 때 동일하게 유지되는 것과 변경되는 것을 살펴봅니다. ### 동일하게 유지되는 것 Stable은 Ethereum 개발 생태계와 완전한 호환성을 유지합니다: | **영역** | **호환성** | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 언어 | Solidity, Vyper | | 도구 | Hardhat, Foundry | | 라이브러리 | ethers.js, web3.js | | 컨트랙트 패턴 | 모든 표준 EVM 규약 (ERC-20, ERC-721, ERC-1155, 프록시 등) | | RPC 인터페이스 | 대부분의 `eth_*` 메서드 지원 (`eth_call`, `eth_sendRawTransaction`, `eth_getBalance`, `eth_getLogs`, `eth_estimateGas` 등). 전체 목록은 [JSON-RPC API](/ko/reference/json-rpc-api)를 참조하세요 | 기존 스마트 컨트랙트, 배포 스크립트, 프론트엔드 통합은 RPC 엔드포인트와 체인 ID를 변경하여 Stable을 대상으로 설정할 수 있습니다. ### 다른 점 네 가지 동작이 Ethereum과 다릅니다. #### 1. 단일 슬롯 완결성(Single-slot finality) Ethereum은 트랜잭션이 최종으로 간주되기 전에 여러 블록 확인을 요구합니다. Stable은 단일 슬롯 완결성을 제공합니다: 트랜잭션은 블록에 포함되는 즉시 최종 확정됩니다. 개발자에게 이는 다음을 의미합니다: * 트랜잭션이 확정된 블록에 나타나면, 그 상태 변경은 최종적이며 되돌릴 수 없습니다. * 애플리케이션은 블록 포함을 정산 확인으로 안전하게 신뢰할 수 있습니다. 결정론적 완결성에도 불구하고, 금융적으로 민감한 흐름을 처리하는 애플리케이션은 다음을 수행해야 합니다: * 종속 작업(예: 잠금 해제, 상환)을 진행하기 전에 RPC 또는 발생한 이벤트를 통해 트랜잭션 성공을 확인하세요. * 일시적인 제출 또는 RPC 오류를 처리하기 위해 자동화 및 배치 작업에 재시도 및 조정 로직을 구현하세요. #### 2. 가스 토큰: USDT0 Stable에서는 트랜잭션 수수료가 변동성이 큰 네이티브 토큰이 아닌 USDT0로 지불됩니다. 이는 USDT로 표시되는 예측 가능한 낮은 가스 비용을 제공합니다. * 사용자는 트랜잭션을 제출하기 위해 지갑에 USDT0가 필요합니다. * 트랜잭션의 `value` 필드는 Ethereum에서 ETH를 보내는 것과 유사하게 USDT0를 보내는 데 여전히 작동합니다. * 자세한 내용은 [가스로서의 USDT](/ko/explanation/usdt-as-gas-token)를 참조하세요. #### 3. 우선순위 팁 없음 Stable은 단일 구성 요소 가스 모델을 사용합니다. 팁 기반 트랜잭션 순서 지정이 없습니다. * `maxPriorityFeePerGas`는 무시됩니다(항상 0). * 트랜잭션 순서는 수수료 입찰의 영향을 받지 않습니다. * 지갑은 우선순위 팁 입력 필드를 숨기거나 비활성화해야 합니다. * 자세한 내용은 [가스 가격 책정](/ko/explanation/gas-pricing)을 참조하세요. #### 4. USDT0 이중 역할 동작 USDT0는 네이티브 가스 토큰과 ERC-20 토큰 두 가지 모두로 기능합니다. 이로 인해 잔액 의미론, 허용량 안전성, 특정 opcode 가정과 관련된 동작 차이가 발생합니다. 자세한 내용은 [Stable에서의 USDT0 동작](/ko/explanation/usdt0-behavior)을 참조하세요. ### 빠른 비교 | **매개변수** | **Stable** | **Ethereum** | | :------------------------------ | :--------- | :----------- | | 가스 토큰 | USDT0 | ETH | | 완결성 | 단일 슬롯 | 다중 블록 확인 | | 블록 시간 | \~0.7초 | \~12초 | | 우선순위 팁 (`maxPriorityFeePerGas`) | 무시됨(항상 0) | 순서 지정에 사용 | | EIP-1559 트랜잭션 형식 | 지원됨 | 지원됨 | | EVM 호환성 | 완전 | 해당 없음 | ### 다음 권장 사항 * [**가스로서의 USDT**](/ko/explanation/usdt-as-gas-token) — 가스를 위해 ETH를 대체하는 자산 모델을 이해하세요. * [**가스 가격 책정**](/ko/explanation/gas-pricing) — 단일 구성 요소 수수료 모델을 자세히 검토하세요. * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 이중 역할 자산 의미론, 허용량 안전성, `EXTCODEHASH` 동작에 대해 컨트랙트를 감사하세요. ## Ethereum 생태계와의 호환성 Stable은 Ethereum Virtual Machine (EVM)과 완벽하게 호환되어, 개발자들이 익숙한 도구, 라이브러리 및 컨트랙트 패턴을 수정 없이 사용할 수 있습니다. 이를 통해 기존 애플리케이션의 원활한 마이그레이션과 이미 Ethereum 생태계에서 개발 중인 팀의 간편한 온보딩을 보장합니다. **주요 호환성 기능** * **언어**: 스마트 컨트랙트 개발을 위해 Solidity와 Vyper를 지원합니다. * **도구**: Hardhat 및 Foundry와 같은 표준 프레임워크와 즉시 사용 가능합니다. * **라이브러리**: ethers.js, web3.js 및 기타 일반적인 JSON-RPC 클라이언트와 완벽하게 호환됩니다. * **컨트랙트 패턴**: ERC-20 승인, 이벤트 발생 및 액세스 제어 메커니즘을 포함한 표준 EVM 규약을 준수합니다. * **RPC 인터페이스**: Ethereum 네트워크에서 사용되는 동일한 JSON-RPC 메서드를 제공하여 기존 통합 및 인덱서가 코드 변경 없이 작동할 수 있도록 합니다. ## 실행 ### Stable EVM Stable EVM **Stable EVM**은 Stable의 Ethereum 호환 실행 계층으로, MetaMask와 같은 기존 Ethereum 도구 및 지갑을 사용하여 체인과 원활하게 상호작용할 수 있도록 합니다. Stable EVM은 EVM의 개발자 경험과 StableSDK의 모듈러한 고성능 인프라를 결합합니다. Stable EVM과 StableSDK 간의 간극을 메우기 위해, Stable EVM은 일련의 **프리컴파일**을 도입합니다. 이 프리컴파일들은 EVM 스마트 컨트랙트가 StableSDK의 네이티브 모듈 기능에 접근할 수 있도록 하며, 핵심 체인 로직을 안전하고 아토믹하게 호출할 수 있게 합니다. 이러한 설계는 스마트 컨트랙트가 토큰 전송, 스테이킹, 거버넌스 참여와 같은 특별한 작업을 수행할 수 있도록 합니다. ### 향후 로드맵 1: 낙관적 병렬 실행 역사적으로, 블록체인 시스템은 각 트랜잭션이 순차적으로 하나씩 처리되는 ‘순차 실행’에 의존해 왔으며, 이는 모든 노드에서 결정론적인 상태를 보장하기 위함이었습니다. 이러한 설계는 일관성을 보장하지만, 동시에 처리량과 확장성에 심각한 제약을 만듭니다. 특히 현대 블록체인이 수만 건의 TPS를 지원하고자 할 때 더욱 그러합니다. 이 제약을 극복하기 위해, Stable은 **낙관적 병렬 실행(Optimistic Parallel Execution, OPE)** 을 가능하게 하는 검증된 병렬 실행 엔진인 **Block-STM**을 채택하고 있습니다. 이를 통해 트랜잭션은 결정론성을 유지하면서 병렬로 실행될 수 있으며, 성능이 크게 향상됩니다. #### Block-STM 작동 방식 Block-STM은 낙관적 동시성 제어 메커니즘을 활용합니다. 트랜잭션은 충돌이 없다는 가정 하에 병렬로 먼저 실행되며, 이후 검증 단계에서 충돌이 감지되면 재실행을 통해 처리됩니다. 이 과정은 다음 다섯 가지 핵심 기술에 기반합니다: **1. 다중 버전 메모리 구조 (Multi-Version Memory Structure)** Block-STM은 각 메모리 키에 대해 여러 버전을 저장합니다: * 각 트랜잭션은 이전 트랜잭션이 커밋한 최신 버전을 읽습니다. * 실행 중의 읽기와 쓰기 모두 버전이 지정됩니다. * 이후 검증 시 이러한 버전들에 대한 일관성 검사를 통해 충돌 여부를 판단합니다. **2. Read-Set / Write-Set 기반 검증** * 각 트랜잭션은 실행 중 읽은 키와 버전을 Read-Set에 기록합니다. * 실행이 끝나면 Write-Set이 다중 버전 메모리에 기록됩니다. * 검증 과정에서 다른 트랜잭션이 해당 Read-Set의 키를 수정한 경우, 이는 충돌로 간주되어 트랜잭션은 중단되고, 반복 시도 번호(incarnation number)를 증가시켜 재실행됩니다. **3. ESTIMATE 마커를 이용한 빠른 충돌 감지** * 트랜잭션이 실패하면 해당 Write-Set은 ESTIMATE 플래그로 마크됩니다. * 다른 트랜잭션이 ESTIMATE로 표시된 값을 읽으면 (**`READ_ERROR`** 가 발생하면) 즉시 중단하고 재실행을 대기합니다. * 이를 통해 전체 트랜잭션을 다시 실행하지 않고도 의존 관계를 빠르게 식별할 수 있습니다. **4. 사전 정의된 트랜잭션 순서** * 블록 내 모든 트랜잭션은 사전 정의된 결정론적 순서에 따라 실행됩니다. * 검증 및 커밋 단계 또한 동일한 순서를 따릅니다. * 이를 통해 병렬 실행 환경에서도 모든 노드가 동일한 최종 상태에 도달하도록 보장합니다. **5. 협업 스케줄러** * 협업 스케줄러는 실행 및 검증 워커 간에 작업을 스레드-세이프 방식으로 분배합니다. * 인덱스가 낮은 트랜잭션을 우선 처리하여, 조기 커밋을 가속화하고 재실행을 최소화합니다. * 스케줄러는 트랜잭션의 반복 시도 번호를 관리하며, 성공적으로 커밋될 때까지 반복 실행합니다. #### Block-STM의 주요 이점 * **락 없이 병렬성 실현**: MVCC(Multi-Version Concurrency Control)를 활용하여, Block-STM은 뮤텍스 락 없이 여러 트랜잭션의 동시 읽기/쓰기를 가능하게 합니다. 충돌 검사는 실행 이후에만 수행되므로, 초기 처리 단계에서 최대 처리량을 확보할 수 있습니다. * **ESTIMATE 마커를 통한 최소한의 오버헤드**: 실패한 트랜잭션은 자신의 Write-Set에 ESTIMATE 마커를 설정하여, 여기에 의존하는 트랜잭션을 조기에 중단하고 불필요한 실행을 피할 수 있도록 합니다. 이로 인해 유효한 실행 경로에 더 빠르게 도달할 수 있습니다. * **효율적인 스케줄링 및 우선 커밋**: 협업 스케줄러를 통해 인덱스가 낮은 트랜잭션을 우선 커밋함으로써, 재시도를 최소화합니다. 이는 전체 처리량을 개선하고 실행 주기를 단축시킵니다. * **결정론성과 합의 호환성**: 모든 트랜잭션은 고정된 순서를 따르므로, 재실행된 트랜잭션도 결국 동일한 순서로 커밋됩니다. 이를 통해 모든 노드 간 안전하고 결정론적인 상태 합의가 가능하며, 병렬화된 환경에서도 합의 무결성이 유지됩니다. #### Stable의 OPE Optimistic Parallel Execution on Stable Stablechain은 **낙관적 병렬 실행(OPE)** 을 실행 계층의 핵심 기능으로 통합할 예정이며, 이는 **낙관적 블록 처리(Optimistic Block Processing, OBP)** 와 함께 사용됩니다. OPE와 OBP는 서로 보완적이지만 본질적으로 다른 전략이라는 점에 유의해야 합니다. #### OBP란 * OBP는 병렬 전략이 아니라, 실행 타이밍과 관련이 있습니다. * `ProcessProposal` 단계에서, Stable은 다른 노드에 블록이 전파되는 동안 해당 블록을 미리 실행합니다. * 결과 상태는 메모리에 캐시되며, 이는 `FinalizeBlock` 단계에서 재사용되어 불필요한 시간 낭비와 중복된 연산을 줄입니다. OPE와 OBP를 결합함으로써, Stable은 실행 지연과 리소스 충돌을 모두 최소화하고, 높은 트랜잭션 부하 환경에서도 우수한 성능을 제공합니다. #### 예상 성능 향상 내부 벤치마크에 따르면, **Block-STM 기반 OPE**와 **StableDB** 통합을 통해 Stable은 전체 트랜잭션 처리량에서 **최소 2배 이상의 향상**을 달성할 수 있습니다. ### 향후 로드맵 2: StableVM++ OPE 및 OBP와 같은 노력은 *다수의 트랜잭션을 동시에 실행하는 방식을 최적화* 하는 데 초점을 맞추지만, 또 하나의 핵심 성능 요소는 **각 개별 트랜잭션을 얼마나 효율적으로 처리할 수 있는가**입니다. Stable은 현재 실행 속도를 높이기 위해 대체 EVM 구현체들을 탐색하는 중입니다. 그중에서도 C++로 작성된 고성능 EVM인 **EVMONE**이 기존의 Go 기반 EVM을 대체할 유력한 후보로 떠오르고 있습니다. EVMONE으로의 전환은 이론적인 벤치마크 기준으로 **최대 6배의 EVM 실행 성능 향상**을 가져올 것으로 기대됩니다. ## Finality 규칙 및 호환성 보장 Stable은 EVM 기반 실행 환경에서 트랜잭션을 처리합니다. 트랜잭션이 블록에 포함되면 그 효과가 상태에 적용되고 애플리케이션, 컨트랙트 및 인덱서에 즉시 표시됩니다. #### 실행 확인 트랜잭션은 다음 조건을 충족하면 **확인됨**으로 간주됩니다: * 생성된 블록에 성공적으로 포함됨 * 상태 변경(잔액, 스토리지, 이벤트)을 RPC를 통해 확인할 수 있음 퍼블릭 테스트넷 단계 동안: * 확인된 상태는 애플리케이션 로직에 유효한 것으로 취급되어야 합니다 * 블록 연속성을 추적하기 위해 모니터링 시스템을 사용해야 합니다 #### Settlement 고려사항 Stable은 싱글 슬롯 finality를 제공하므로, 트랜잭션은 유효한 블록에 포함되는 즉시 확정됩니다. **개발자에게 이것은 다음을 보장합니다:** * 트랜잭션이 확인된 블록에 나타나면 상태 변경은 최종적이며 되돌릴 수 없습니다. * 애플리케이션은 블록 포함을 settlement 확인으로 안전하게 신뢰할 수 있습니다. **결정론적 finality가 있더라도, 재정적으로 민감한 플로우를 처리하는 애플리케이션은 다음을 수행해야 합니다:** * 종속 작업(예: 잠금 해제, 상환)을 진행하기 전에 RPC 또는 발행된 이벤트를 통해 트랜잭션 성공을 확인합니다. * 일시적인 제출 또는 RPC 오류를 처리하기 위해 자동화 및 일괄 작업에 대한 재시도 및 조정 로직을 구현합니다. #### Compatibility Commitments Stable은 테스트넷 성장 단계 전반에 걸쳐 개발자를 위한 일관된 실행 환경을 유지할 것을 목표로 합니다. **Current Commitments:** * 게시된 시스템 모듈 인터페이스와 실행 동작은 명시적으로 언급되지 않는 한 안정적으로 유지됩니다 * 잠재적으로 파괴적인 변경 사항은 다음과 같이 처리됩니다: * 사전 공지 * 릴리스 및 변경 로그에 문서화 * 필요한 경우 마이그레이션 지침 제공 향후 업데이트에는 다음이 포함될 예정입니다: * 공식 호환성 정책 * 개발자 대상 기능에 대한 변경 수준 분류 * 버전 전환에 대한 명확한 처리 지침 ## 자금의 흐름 Stable은 스테이블코인 결제를 위해 특별히 설계된 최초의 블록체인입니다. 이 네트워크는 고처리량, 저지연 스테이블코인 거래에 최적화되어 있으며, USDT로 즉시 정산되는 P2P 결제와 가맹점 결제 수용을 제공합니다. 애플리케이션 계층의 가스 후원 및 면제를 통해 제공업체는 최종 사용자에게 수수료 없는 경험을 제공할 수 있으며, 블록체인 시스템의 복잡성을 추상화하면서 주류 결제 네트워크와 같은 느낌을 줍니다. 이 페이지는 Stable에서 자금의 완전한 생명주기를 설명합니다. USDT가 어떻게 네트워크에 진입하고, 참여자 간에 이동하며, 다시 법정화폐 레일로 빠져나가는지를 다룹니다. ### 1. 고객 입금 (온램프) 사용자는 세 가지 주요 채널 중 하나를 통해 네트워크로 자금을 들여옵니다: * **암호화폐 전송**: 주요 암호화폐는 Stable에서 USDT0로 브리징되거나 변환됩니다. USDT0는 USDT를 위한 옴니체인 표준이자 네트워크의 주요 형식입니다. * **법정화폐 온램프**: 카드, ACH 또는 현지 결제 수단이 법정화폐를 USDT0로 변환하여 사용자의 지갑으로 직접 전달합니다. * **CEX 출금**: 사용자가 지원되는 중앙화 거래소에서 USDT를 출금하면서 Stable을 목적지 네트워크로 선택합니다. 거래소는 사용자의 지갑으로 직접 정산합니다. 모든 경우에 최종 상태는 동일합니다: 사용자의 지갑이 Stable에서 직접 USDT(USDT0 형태)를 보유합니다. ### 2. P2P / 가맹점 전송 (온체인 페이인) 자금이 Stable에 들어오면, 고객은 다른 사용자나 가맹점에게 USDT를 직접 전송합니다. 온체인 전송의 주요 특성: * **즉시 정산**: 전송이 온체인에서 즉시 정산됩니다. * **비수탁형**: 비수탁형 지갑의 경우, 출발지와 목적지 사이에서 어떠한 PSP나 중개자도 사용자 잔액을 건드리지 않습니다. * **단일 자산**: USDT가 가스이자 정산 자산이기 때문에, 흐름에 추가 토큰이 없고 숨겨진 스프레드도 없습니다. * **제로 가스 옵션**: 가스 면제를 통해 최종 사용자가 블록체인 수수료를 관리할 필요 없이 자금을 이동할 수 있습니다. 자세한 내용은 [가스 면제](/ko/reference/gas-waiver-api)를 참조하세요. ### 3. 사용자 / 가맹점 잔액 가맹점은 자신의 직접적인 통제 하에 있는 Stable 지갑으로 USDT를 받습니다. 자금은 사용자 또는 가맹점의 수탁 하에 온체인에 보관됩니다. 이러한 지갑은 사용자를 대신하여 결제 제공업체가 생성하고 관리할 수 있습니다. ### 4. 가맹점 출금 (오프램프 / 페이아웃) 가맹점 또는 사용자가 오프체인 법정화폐 정산을 요청할 때: 1. 제공업체가 뱅킹 또는 페이아웃 레일을 통해 변환(USDT → 법정화폐)을 시작합니다. 2. 자금이 가맹점이 선택한 계좌로 입금됩니다. 제공업체는 생태계 내부 전송 중이 아니라 가맹점의 현금화 시에만 흐름에 다시 진입합니다. 일상적인 P2P 흐름에는 중개가 필요하지 않으며, 제공업체는 입금(가맹점 계좌로의 USDT 전송) 또는 출금(USDT → 법정화폐) 시에만 참여합니다. ### 교차 자산 거래 Stable은 결제자가 USDT가 아닌 암호화폐를 보유한 시나리오도 지원합니다. #### 사용자가 다른 암호화폐로 거래 사용자는 통합 거래소, 브로커 또는 온체인 DEX를 통해 다른 암호화폐(예: BTC 또는 ETH)를 보유하거나 거래할 수 있습니다. 결제 시점에 시스템은 선택된 암호화폐를 자동으로 USDT로 변환하며, 이는 가맹점의 Stable 지갑으로 전송됩니다. 사용자가 선호하는 자산과 관계없이 모든 온체인 정산은 계속 USDT로 이루어집니다. #### 가맹점의 암호화폐 결제 수용 가맹점은 여러 암호화폐를 직접 수용하거나 관리할 필요가 없습니다. 그들은 항상 자신의 Stable 지갑으로 USDT를 받으며, 네트워크 전반에 걸쳐 단일 정산 통화를 유지합니다. 이 설계는 가맹점의 환율 노출을 최소화하고 대사 및 보고를 단순화합니다. #### 변환에서의 제공업체 역할 변환 로직(예: BTC → USDT)은 거래소 파트너, 유동성 공급자 또는 결제 제공업체 자체의 자금 관리부에서 처리할 수 있습니다. 가맹점은 변동성이나 유동성 위험으로부터 차단되며, 오직 USDT만 받습니다. ### 다음 권장 사항 * [**가스로서의 USDT**](/ko/explanation/usdt-as-gas-token) — USDT0가 Stable에서 네이티브 가스와 ERC-20 잔액 모두로 어떻게 작동하는지 이해합니다. * [**Stable로 브리징**](/ko/explanation/usdt0-bridging) — USDT0가 OFT Mesh 또는 Legacy Mesh를 통해 다른 체인에서 Stable로 어떻게 이동하는지 살펴봅니다. * [**첫 USDT0 전송하기**](/ko/tutorial/send-usdt0) — 표준 EVM 도구를 사용하여 테스트넷에서 USDT0 전송을 제출합니다. ## Gas 가격 책정 Stable은 단일 구성 요소 gas 수수료 모델을 사용합니다: * **우선순위 팁 없음 (maxPriorityFeePerGas는 무시됨)** * 수수료는 순수하게 기본 실행 비용에 기반합니다 * 수수료는 **USDT0**로 지불됩니다 근거: * Stable은 예측 가능한 결제를 위해 수수료 변동성을 제거합니다 * "팁"과 채굴자 인센티브에 대한 사용자 혼란을 제거합니다 개발자에 미치는 영향: * 개발자는 트랜잭션 가속화를 위해 **우선순위 수수료**에 의존해서는 안 됩니다 * 지갑은 팁 입력 필드를 숨기거나 비활성화해야 합니다 * Gas 추정 도구는 Stable RPC 가격 책정 로직을 참조해야 합니다 ## Gas Waiver ### 요약 Gas Waiver는 소수의 거버넌스 승인 주소("면제자")가 `gasPrice = 0`인 트랜잭션을 제출할 수 있도록 허용하여 Stable에서 가스리스 최종 사용자 트랜잭션을 가능하게 합니다. Stable은 현재 파트너가 프로토콜별 래퍼 로직을 구현하지 않고도 가스리스 UX를 제공하기 위해 통합할 수 있는 면제자 서비스("면제자 서버")를 운영하고 있습니다. 본 문서는 Gas Waiver 메커니즘, 트랜잭션 형식, 거버넌스 제어 및 파트너를 위한 면제자 서버 API를 명시합니다. ### 범위 본 명세는 다음을 다룹니다: * 가스 면제 트랜잭션에 대한 프로토콜 수준 규칙 * 래퍼 트랜잭션 메커니즘 및 마커 주소 * 거버넌스 제어 권한 부여 및 허용된 대상 * 서명된 사용자 트랜잭션 제출을 위한 면제자 서버 인터페이스 ### 정의 * **Waiver(면제자)**: 검증자 거버넌스를 통해 온체인에 등록된 이더리움 주소로, 가스 면제 트랜잭션을 제출할 권한이 있습니다. * **InnerTx(내부 트랜잭션)**: `gasPrice = 0`인 최종 사용자의 서명된 트랜잭션입니다. * **WrapperTx(래퍼 트랜잭션)**: 면제자가 서명한 트랜잭션으로, 사용자의 `InnerTx`를 체인으로 전송하고 실행을 승인합니다. * **Marker address(마커 주소)**: 면제자 래퍼 트랜잭션을 식별하는 데 사용되는 센티널 주소: `0x000000000000000000000000000000000000f333`. * **AllowedTarget(허용된 대상)**: 면제자를 특정 컨트랙트 주소 및 메서드 선택자로 제한하는 정책입니다. ### 개요 Gas Waiver는 래퍼 트랜잭션 패턴을 사용합니다: 1. 사용자가 `gasPrice = 0`인 `InnerTx`에 서명합니다. 2. 면제자가 `InnerTx`를 `WrapperTx`로 래핑하여 브로드캐스트합니다. 3. 검증자는 마커 트랜잭션을 감지하고, 면제자 권한 및 정책 제약 조건을 확인한 후 임베디드된 `InnerTx`를 실행합니다. Stable은 온체인에 승인된 면제자로 등록된 면제자 서비스(면제자 서버)를 운영합니다. 파트너는 면제자 서버 API와 통합하여 서명된 `InnerTx` 페이로드를 제출합니다. ### 프로토콜 명세 #### 마커 주소 라우팅 다음 조건을 만족하는 경우에만 트랜잭션이 면제자 래퍼 트랜잭션으로 처리됩니다: * `to == 0x000000000000000000000000000000000000f333`. 프로토콜은 트랜잭션 `data` 필드를 인코딩된 내부 트랜잭션 페이로드로 해석하고 아래의 면제자 검증 규칙을 사용하여 처리합니다. #### 권한 부여 및 정책 검사 각 후보 래퍼 트랜잭션에 대해 검증자는 다음을 시행해야 합니다: 1. **면제자 권한 부여** * `WrapperTx.from`은 거버넌스를 통해 온체인에 등록된 면제자 주소여야 합니다. 2. **가스 면제** * `WrapperTx.gasPrice`는 `0`이어야 합니다. * `InnerTx.gasPrice`는 `0`이어야 합니다. 3. **대상 허용 목록** * `InnerTx.to` 및 `InnerTx.data`에서 추출된 메서드 선택자는 면제자의 `AllowedTarget` 정책에 의해 허용되어야 합니다. 4. **Value 제한** * `WrapperTx.value`는 `0`이어야 합니다. 검사가 실패하면 래퍼 트랜잭션은 거부되어야 하며 내부 트랜잭션은 실행되지 않아야 합니다. #### 실행 의미론 모든 검사가 통과되면: 1. 프로토콜은 사용자의 `from`, `nonce` 및 호출 의미론을 보존하면서 사용자로서 `InnerTx`를 실행합니다. 2. 가스 회계는 면제자 메커니즘에 의해 처리됩니다: 사용자는 가스를 지불하지 않으며, 면제자 트랜잭션은 기능의 정의에 따라 `gasPrice = 0`을 사용합니다. 3. 래퍼 트랜잭션은 `InnerTx`의 실행을 커버할 충분한 `gasLimit`를 제공해야 합니다(언래핑 및 검증 오버헤드 포함). ### 트랜잭션 형식 #### WrapperTx 래퍼 트랜잭션은 면제자가 서명하고 마커 주소로 전송됩니다. ```javascript WrapperTx { from: waiver_address, to: 0x000000000000000000000000000000000000f333, value: 0, // 0이어야 함 data: RLP(InnerTx), // RLP 인코딩된 내부 트랜잭션 gasPrice: 0, // 0이어야 함 gasLimit: sufficient_for_inner, // 내부 실행 + 오버헤드를 커버해야 함 nonce: waiver_nonce } ``` #### InnerTx 내부 트랜잭션은 최종 사용자가 서명합니다. ```javascript InnerTx { from: user_address, to: target_contract, value: value, data: call_data, gasPrice: 0, // 0이어야 함 gasLimit: execution_gas, nonce: user_nonce } ``` ### 거버넌스 제어 액세스 면제자 권한 부여는 검증자 거버넌스에 의해 온체인에서 관리됩니다. 거버넌스 제어는 다음을 제공합니다: * 면제자 주소의 검토 가능한 권한 부여 * 면제자 등록 및 업데이트의 온체인 투명성 * 철회 기능 * `AllowedTarget`을 통한 면제자별 범위 지정 ### 보안 모델 #### 최종 사용자 서명 무결성 사용자는 `InnerTx`에 서명합니다. 면제자는 서명을 무효화하지 않고는 내부 트랜잭션 페이로드를 수정할 수 없습니다. 파트너는 여전히 사용자가 의도된 트랜잭션 페이로드에만 서명하도록 보장해야 합니다. #### 신뢰 경계 파트너가 면제자 서버를 통해 제출을 라우팅하는 경우 Gas Waiver는 서비스 종속성을 도입합니다: * 서비스의 가용성은 가스리스 트랜잭션을 제출할 수 있는 능력에 영향을 미칩니다. * 권한 부여는 온체인에 유지됩니다. 등록된 면제자 주소만 유효한 래퍼 제출을 생성할 수 있습니다. ### 파트너 통합 파트너는 다음과 같이 통합합니다: 1. 사용자로부터 서명된 `InnerTx`를 수집합니다(`gasPrice = 0`). 2. 서명된 내부 트랜잭션을 면제자 서버 API에 제출합니다. 3. 스트리밍된 결과를 처리하고 최종 사용자에게 트랜잭션 해시를 표시합니다. ### 면제자 서버 #### 개요 면제자 서버는 서명된 사용자 `InnerTx` 페이로드를 면제자 승인 래퍼 트랜잭션으로 래핑하고 브로드캐스트합니다. 파트너는 래퍼 트랜잭션을 구성하거나 면제자 주소를 운영할 필요가 없습니다. #### 엔드포인트 및 기본 URL 기본 URL: * 메인넷: 미정 * 테스트넷: `https://waiver.testnet.stable.xyz` #### 인증 상태 확인을 제외한 모든 엔드포인트는 Bearer 토큰 인증이 필요합니다: ``` Authorization: Bearer ``` #### API ##### GET `/v1/health` 상태 확인 엔드포인트입니다. 인증: 없음. ##### POST `/v1/submit` 서명된 내부 트랜잭션 배치를 제출합니다. 인증: 필수(`Bearer`). 요청 본문: ```json { "transactions": ["0x", "0x"] } ``` 응답은 NDJSON(줄 바꿈 구분 JSON)으로 스트리밍됩니다. 각 줄은 제출된 트랜잭션 인덱스에 해당합니다. 예시: ```json {"index":0,"id":"abc123","success":true,"txHash":"0x..."} {"index":1,"id":"def456","success":false,"error":{"code":"VALIDATION_FAILED","message":"invalid signature"}} ``` ##### GET `/v1/submit` 스트리밍 제출을 위한 WebSocket 인터페이스입니다. 인증: 필수(`Bearer`). #### 통합 예시 ```javascript const WAIVER_SERVER = "https://waiver.testnet.stable.xyz"; async function submitGaslessTransaction(signedInnerTxHex, apiKey) { const response = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex], }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); console.log(result); } } } ``` #### 사용자 InnerTx 생성 파트너는 `gasPrice = 0`인 `InnerTx`를 구성한 후 사용자 서명을 수집할 책임이 있습니다. 예시: ```javascript import { ethers } from "ethers"; async function createInnerTx(userWallet, contractAddress, callData, nonce) { const innerTx = { to: contractAddress, data: callData, value: value, gasPrice: 0, // 면제를 위해 0이어야 함 gasLimit: 100000, nonce: nonce, chainId: 2201, // 메인넷: 988, 테스트넷: 2201 }; return await userWallet.signTransaction(innerTx); } ``` #### 오류 코드 * `PARSE_ERROR`: 트랜잭션 파싱 실패 * `INVALID_REQUEST`: 요청 본문 형식 오류 * `BATCH_SIZE_EXCEEDED`: 배치 크기가 허용된 최대값 초과 * `VALIDATION_FAILED`: 트랜잭션 검증 실패 * `BROADCAST_FAILED`: 체인으로 브로드캐스트 실패 * `RATE_LIMITED`: 속도 제한 초과 * `QUEUE_FULL`: 서버 대기열이 용량에 도달 * `TIMEOUT`: 요청 시간 초과 ## 보장된 블록스페이스 ### 비즈니스를 위한 스테이블코인 결제 필요성 스테이블코인이 지속적으로 진화함에 따라, 점점 더 많은 기업들이 이를 결제, 재무 흐름, 국경 간 정산 등 금융 운영에 통합하고 있습니다. 이러한 변화는 특히 안정적인 명목 화폐 접근이 제한된 지역에서 두드러집니다. 아프리카 및 라틴 아메리카와 같은 시장에서는 인플레이션과 환율 통제가 일반적이기 때문에, 스테이블코인은 운영 안정성을 위한 핵심 도구가 되고 있습니다. 오늘날 스테이블코인 트랜잭션은 주로 Ethereum, Solana, Tron과 같은 범용 블록체인에서 발생합니다. 이러한 네트워크는 넓은 composability와 스마트 컨트랙트 지원을 제공하지만, 수수료 예측 가능성이나 실행 보장을 염두에 두고 설계되지 않았습니다. * **Ethereum**: 2022년 5월 1일, Yuga Labs의 “Otherside” NFT 민팅은 Ethereum에서 2억 달러 이상의 가스 수수료가 소각되도록 만들었습니다. 최고 가스는 8,000 gwei를 초과했고, 네트워크 전반에 걸쳐 불안정하고 비결정적인 트랜잭션 비용이 발생했습니다. * **기타 네트워크**: Solana, Base와 같은 저수수료 네트워크에서는 MEV와 차익 거래 기회가 존재함에 따라 대량의 트랜잭션 스팸이 유입됩니다. 이러한 네트워크는 온체인 가치를 포착하려는 봇들로 인해 과부하 상태를 자주 경험하고, 합법적인 사용자에게 네트워크 혼잡 및 성능 저하를 초래합니다. ![Source: MEV and the Limits of Scaling by Flashbots and Rober Miller](/images/share-of-gas.png) *Source: MEV and the Limits of Scaling by Flashbots and Rober Miller* 기업이 대규모로 스테이블코인 결제를 채택하려면, 그 기반 인프라는 결제 신뢰성을 위해 최적화되어야 합니다. 이를 위해서는 모든 조건에서 예측 가능한 트랜잭션 지연 시간과 수수료 안정성이 필요합니다. 이러한 보장이 없다면, 범용 체인에서의 스테이블코인 결제는 여전히 신뢰할 수 없는 상태로 남게 됩니다. ### 보장된 블록스페이스 기업의 안정적이고 신뢰 가능한 결제 운영을 보장하기 위해, Stable은 보장된 블록스페이스에 대한 지원을 도입할 예정입니다. 보장된 블록스페이스는 기업에 고정된 블록 용량을 보장하는 전용 블록 공간 할당 모델로, 네트워크 전반의 상태와 무관하게 적용됩니다. 이를 통해 급여 지급, 정산, 공급업체 결제 등 미션 크리티컬한 트랜잭션이 예측 가능한 지연 시간과 비용으로 실행될 수 있습니다. 이 보장은 다음과 같은 인프라를 통해 강제됩니다: * **보장된 멤풀**: 밸리데이터는 퍼블릭 트래픽과 분리된 전용 멤풀을 운영하며, 여기서 가져온 보장된 트랜잭션들을 우선적으로 처리합니다. * **밸리데이터 단의 커스터마이제이션**: 밸리데이터는 사전에 정의된 만큼의 블록 공간을 예약하여, 결정론적인 포함을 보장합니다. * **전용 RPC 노드**: 보장된 블록스페이스 API는 격리된 RPC 엔드포인트를 통해 트랜잭션을 라우팅하여, 충돌을 줄이고 일관된 처리량을 제공합니다. 보장된 블록스페이스는 사용자에게 다음과 같은 이점을 제공합니다: * 전용 업로드 경로를 통한 독점적 트랜잭션 라우팅으로 블록 공간에 우선 접근 * 네트워크 혼잡 여부와 관계없이 모든 블록에 예약된 용량을 확보하여 보장된 실행 * 밸리데이터의 개방성이나 네트워크 참여에 대한 타협 없이 탈중앙화를 유지 * 높은 네트워크 부하 상황에서도 비즈니스에서 중요한 작업들에 대한 안정적인 온체인 성능 ## 고성능 RPC 고성능 블록체인을 구현하기 위해서는 합의나 블록 생성만을 최적화해서는 충분하지 않습니다. RPC 계층은 블록체인과 사용자를 연결하는 인터페이스로, 사용자 경험에 있어 핵심적인 구성 요소입니다. Stable은 기존 RPC 설계의 한계를 극복하기 위해 새로운 RPC 전용 아키텍처를 제안합니다. ### 왜 고성능 RPC가 중요한가 #### 사용자를 위한 블록체인 진입 관문 **RPC(Remote Procedure Call)** 인터페이스는 사용자가 블록체인과 상호작용하는 주요 방법입니다: * 지갑은 트랜잭션을 브로드캐스트하기 위해 RPC를 사용합니다. * DApp은 UI 렌더링을 위한 온체인 데이터 조회뿐 아니라, 트랜잭션 준비 및 시뮬레이션, 로그 및 이벤트 수집 등을 위해 RPC를 사용합니다. * 익스플로러, 인덱서, 봇 등도 모두 실시간 데이터를 위해 RPC에 의존합니다. 아무리 블록체인이 트랜잭션을 빠르게 처리하고 블록을 신속하게 생성할 수 있어도, 사용자가 RPC 단의 지연 때문에 느리다고 느낀다면 아무 소용이 없습니다. 실제로는 RPC가 전체 사용자 경험에서 병목 지점이 되는 경우가 많습니다. Stable은 고성능 체인을 위한 로드맵에서 **RPC 최적화**를 최우선 과제로 명시하고 있습니다. ### 기존 RPC 아키텍처의 문제점 #### 모놀리식 설계와 리소스 충돌 Traditional RPC Architecture 기존의 RPC 노드는 단순히 기존 풀노드를 재활용하여 RPC 엔드포인트를 추가로 노출시킨 형태입니다. 이는 다음과 같은 단점이 존재한다는 것을 의미합니다: * 체인 동기화와 RPC 요청 처리가 동일한 인스턴스에서 수행됩니다. * RPC를 확장하려면 풀노드를 새로 셋업해야 하며, 이로 인해 상태 동기화(state sync)나 합의 셋업과 같은 리소스 집약적인 작업이 필요합니다. * 합의, 실행, RPC가 모두 동일한 CPU, 메모리, 디스크를 공유합니다. 예를 들어 트랜잭션 부하가 높을 때 한 컴포넌트가 자원을 독점하면, 나머지 컴포넌트가 충분한 리소스를 얻지 못해 RPC 성능이 저하됩니다. 또한 기존 아키텍처는 읽기 위주와 쓰기 위주의 연산을 구분하지 않고 동일한 방식으로 처리합니다. 예를 들어 `eth_getBalance`와 같은 읽기 쿼리는 쓰기 트랜잭션보다 훨씬 빈도 수가 많지만, 처리 방식에는 차이가 없습니다. 이러한 구조는 본질적으로 비효율적이며 확장성이 떨어집니다. ### Stable의 RPC 아키텍처 Stable은 읽기와 쓰기를 분리하고 각 경로를 독립적으로 최적화하는 split-path RPC 아키텍처를 도입합니다. Stable RPC Architecture #### 핵심 원칙 * RPC를 기능에 따라 효율적이고 경량화된 RPC 노드로 분리 * 경량 RPC를 엣지 노드(edge nodes)로 배포하여 확장성 강화 * 각 기능별 RPC의 데이터 경로를 최적화하여 지연을 줄이고 더 직접적이고 효율적인 데이터 구조로 처리 #### 성능 향상 새로운 읽기 전용 RPC 경로에 대한 내부 벤치마크 결과는 다음과 같습니다. * 동일한 환경에서 10,000 RPS 이상의 처리량, 100ms 미만의 엔드투엔드 레이턴시 * 전체 상태 동기화나 합의 부담 없이 선형 확장 가능한 엣지 노드 Stable의 새로운 RPC 아키텍처는 높은 트래픽에서도 훨씬 더 부드럽고 빠른 사용자 경험을 제공합니다. ### 향후 계획 #### EVM View 호출 최적화 현재 진행 중인 리서치 중 하나는 EVM view 연산 (`eth_call`)에 대한 전용 지원입니다: * 이 연산은 트랜잭션 커밋이나 상태 업데이트를 필요로 하지 않습니다. * 현재의 상태에 대한 스냅샷만을 사용하는 경량화된 무상태(stateless) 환경에서 실행이 가능합니다. * 이러한 작업을 위한 특화된 RPC 노드를 설계하면, 응답 시간을 더욱 단축시키고 주요 전체 노드의 부하를 줄일 수 있습니다. #### 인덱서와 노드의 결합 인덱서를 노드에 직접 통합하면 dApp에 최대한 빠른 데이터 제공이 가능해집니다. * 기존 아키텍처: 노드 → RPC → 인덱서 (예: The Graph) → 스토리지 → dApp * 제안된 아키텍처: 인덱서 + 노드 → DB → dApp * 이 아키텍처는 인덱서가 노드에 네이티브로 통합되어 있기 때문에, 네트워크 통신 단계를 제거하고 데이터 전달 속도를 획기적으로 향상시킵니다. ## 개요 Stable은 USDT0가 네이티브 가스 토큰인 EVM 호환 Layer 1입니다. 대부분의 이더리움 도구, 라이브러리, 컨트랙트 패턴은 수정 없이 작동합니다. RPC를 Stable로 지정하고 체인 ID를 전환하여 연결할 수 있습니다. ### 연결 및 자금 충전 * [**연결**](/ko/reference/connect) — 메인넷 및 테스트넷 체인 ID, RPC 엔드포인트, 블록 익스플로러. * [**테스트넷 지갑에 자금 충전**](/ko/how-to/use-faucet) — 포셋을 통해 테스트넷 USDT0를 받거나 Sepolia에서 브리지하세요. * [**Stable SDK**](/ko/explanation/sdk-overview) — 전송, 브리지, 스왑을 위한 타입이 지정된 TypeScript 클라이언트를 사용하세요. ### USDT0로 구축하기 * [**첫 USDT0 전송하기**](/ko/tutorial/send-usdt0) — TypeScript 예제와 함께하는 네이티브 및 ERC-20 전송. * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 이중 역할 잔액 조정, 컨트랙트 설계 요구사항, 마이그레이션 체크리스트. * [**이더리움과의 차이점**](/ko/explanation/ethereum-comparison) — 단일 슬롯 완결성, USDT0 가스, 우선순위 팁 없음. * [**제로 가스 트랜잭션**](/ko/how-to/integrate-gas-waiver) — Waiver Server API를 통한 Gas Waiver 통합. ### 결제 * [**ERC-3009**](/ko/explanation/erc-3009) — Transfer With Authorization: 온체인 정산 프리미티브. * [**x402**](/ko/explanation/x402) — 계정이나 API 키가 필요 없는 HTTP 네이티브 결제. * [**P2P 결제**](/ko/reference/p2p-payments) — 네이티브 및 ERC-3009 위임 전송. ### 에코시스템 공급자와 인프라가 이미 Stable에서 운영되고 있습니다: 브리지, [표준 Uniswap v3 배포](/ko/reference/dexes), 오라클, RPC, 지갑, 커스터디 등. 전체 목록은 [에코시스템](/ko/reference/bridges) 섹션을 둘러보세요. ## 주요 기능 Stable은 USDT 기반 활동을 원활하게 지원하기 위해 설계된 고성능 블록체인입니다. **위임 지분증명 (dPoS)** 메커니즘 위에 구축된 Stable은 **완전한 EVM 호환성**을 제공하며, **1초 미만의 블록 생성 시간**을 통해 빠르고 신뢰성 높은 트랜잭션 완결성을 달성합니다. **USDT에 특화**된 네트워크인 Stable은 사용자 경험을 최적화하는 다양한 USDT 전용 기능을 제공합니다. #### 주요 기능 * **1초 미만의 블록 완결성**: 단일 슬롯 완결성와 함께 1초 미만의 블록 타임을 달성합니다. * **100% EVM 호환성**: Ethereum의 스마트 컨트랙트 및 툴링을 완벽히 지원합니다. * **USDT를 가스 토큰으로 사용**: USDT0를 네이티브 가스 토큰으로 사용합니다. USDT0는 가스 결제와 가치 전송을 위한 네이티브 자산으로 동시에 기능하며, `approve`, `transfer`, `transferFrom`, `permit`을 지원하는 ERC20 토큰으로도 동작합니다. * **USDT0 크로스체인 브릿지**: Ethereum, Arbitrum, HyperEVM 같은 EVM 체인뿐 아니라 Tron 등 다른 체인에서도 USDT0를 Stable로 브릿지할 수 있습니다. * **Stable Pay을 통한 Web2.5 수준의 사용자 경험**: Stable Pay을 통해 직관적이고 원활한 사용자 경험을 제공합니다. 향후 Stable은 USDT의 활용성과 네트워크 효율을 더욱 높이기 위한 기능을 추가로 도입할 예정입니다. **USDT 전송 집계** 기능은 여러 개의 USDT 관련 트랜잭션을 하나의 번들로 묶어 처리량을 향상할 것이며, **보장된 블록스페이스**는 기관이 예측 가능하고 안정적인 환경에서 USDT를 사용할 수 있도록 지원할 것입니다. 이러한 업그레이드를 통해 Stable은 USDT의 중심지로 자리매김할 것입니다. ## 학습하기 ### 기초 * [**개요**](/ko/explanation/overview) — Stable이 무엇이며 이 문서를 어떻게 읽어야 하는지 설명합니다. * [**주요 기능**](/ko/explanation/key-features) — 핵심 사양: 단일 슬롯 완결성, 가스로서의 USDT0, 완전한 EVM 호환성. * [**이더리움과의 차이점**](/ko/explanation/ethereum-comparison) — 이더리움에서 포팅할 때 유지되는 것과 바뀌는 것. * [**핵심 개념**](/ko/explanation/core-concepts) — USDT0의 이중 역할, 보장된 블록 공간, 전송 애그리게이터, 완결성. ### USDT0 동작 * [**Stable에서의 USDT0 동작**](/ko/explanation/usdt0-behavior) — 이중 역할 잔액, 조정 이벤트, 컨트랙트 설계 규칙. * [**가스로서의 USDT**](/ko/explanation/usdt-as-gas-token) — Stable이 가스 지불에 USDT0를 사용하는 이유와 그것이 수수료에 미치는 의미. * [**자금 흐름**](/ko/explanation/flow-of-funds) — USDT가 Stable 전반에서 처음부터 끝까지 어떻게 이동하는지. * [**USDT0 기능**](/ko/explanation/usdt-features-overview) — 모든 USDT0 전용 기능과 각각으로 연결되는 링크. ### 아키텍처 * [**기술 개요**](/ko/explanation/tech-overview) — 합의, 실행, 데이터베이스, RPC 계층을 한 페이지에 담았습니다. * [**핵심 최적화**](/ko/explanation/core-optimization-overview) — 1초 미만의 완결성을 뒷받침하는 성능 작업. * [**완결성**](/ko/explanation/finality) — 단일 슬롯 완결성, 리오그 동작, 그리고 "확정됨"이 의미하는 것. * [**가스 가격 책정**](/ko/explanation/gas-pricing) — USDT0로 가격이 책정되는 기본 수수료 전용 모델. ### 사용 사례 내러티브 * [**결제**](/ko/explanation/use-case-payments) — Stable이 P2P, 구독, 청구서, 호출당 지불에 적합한 이유. * [**급여 지급**](/ko/explanation/use-case-payroll) — Stable에서 일괄 처리 및 예약된 급여 지급 실행. * [**스폰서드 트랜잭션**](/ko/explanation/use-case-sponsored) — 애플리케이션이 사용자를 위해 가스를 대신 부담하도록 하기. * [**프라이빗 전송**](/ko/explanation/use-case-private) — 곧 출시될 기밀 결제 흐름. ## MPP 세션 세션은 여러 개의 작은 결제를 하나의 온체인 정산으로 묶는 MPP 결제 인텐트입니다. 클라이언트는 에스크로에 자금을 한 번 예치한 후, 각 요청에 대해 저렴한 오프체인 바우처에 서명합니다. 온체인에서는 순액만 정산되므로, 스트리밍 워크로드에서 요청당 1센트 미만의 경제성을 실현할 수 있습니다. ### 세션의 작동 방식 1. **예치.** 클라이언트는 정산 레이어의 세션 에스크로 컨트랙트에 예산을 전송합니다. 에스크로는 자금을 보관하고, 판매자에게 지급하고 나머지를 환불하는 정산 함수를 노출합니다. 2. **요청당 바우처.** 각 유료 요청에 대해 클라이언트는 `(sessionId, cumulativeAmount, nonce, expiry)`를 담은 오프체인 바우처에 서명합니다. 서버는 누적 금액이 단조 증가하며 예치된 잔액 이내인지 확인합니다. 이 단계에서는 온체인 작업이 필요하지 않습니다. 3. **정산.** 세션 종료 시 또는 설정된 주기에 따라, 퍼실리테이터가 최신 바우처를 에스크로에 제출합니다. 에스크로는 판매자에게 누적 금액을 지급하고 남은 잔액을 클라이언트에게 반환합니다. 오직 이 트랜잭션만 체인에 기록됩니다. 세션은 최신 바우처가 정산되거나 바우처 만료 시점이 지나면 종료됩니다. ### 세션과 charge 중 어느 것을 사용할지 | **워크로드** | **최적의 인텐트** | | :---------------------------------------------------------------- | :---------- | | 토큰당 과금 LLM 추론, 프레임당 과금 비디오, 실시간 데이터 스트림. 동일한 판매자에게 보내는 다수의 소액 결제. | Session | | 일회성 유료 API 호출, 단일 구매 리소스, 각 트랜잭션이 독립적인 에이전트 간 커머스. | Charge | 손익분기점은 요청 가격 대비 요청당 온체인 정산 비용이 얼마나 비싼지에 따라 달라집니다. 결제보다 가스에 더 많은 비용을 지불하게 되는 순간, 세션이 적합한 패턴입니다. ### 에이전트 활용 사례 * **토큰 단위 과금 LLM 추론.** 클라이언트가 컴플리션을 스트리밍하고 토큰 배치마다 바우처에 서명하며, 추론 서버는 세션 종료 시 정산합니다. * **프레임 단위 과금 비디오.** 생성된 비디오를 소비하는 에이전트가 N개 프레임마다 바우처에 서명하며, 렌더러는 스트림이 종료될 때 정산합니다. * **실시간 데이터 피드.** 구독자가 오라클 또는 마켓 데이터 스트림의 틱마다 결제하고, 세션 윈도우당 한 번 정산합니다. ### Stable에서의 현황 세션에는 Stable이 현재 제공하지 않는 두 가지 요소가 필요합니다: 1. 예치금을 보관하고 `settleVouchers`(또는 동등한) 함수를 노출하는, USDT0용 세션 인식 에스크로 컨트랙트. 2. 판매자 측에서 바우처를 발행하고 구매자 측에서 검증하며, 에스크로 제출을 배치 처리하는 퍼실리테이터. 이 두 가지가 모두 출시되기 전까지 MPP 세션은 Stable에서 사용할 수 없습니다. 오늘날 고빈도 에이전트 결제를 위한 가장 오버헤드가 낮은 패턴은 Stable의 [가스 면제](/ko/how-to/integrate-gas-waiver)를 통해 제출되는 **charge** 인텐트로, 판매자 측의 트랜잭션당 가스 비용을 제거하고 구매자의 USDT0 잔액을 유일하게 관리할 자산으로 유지합니다. 요청당 charge 패턴은 [Stable에서 MPP 엔드포인트 구축하기](/ko/how-to/build-mpp-endpoint)를 참고하세요. ### 다음 추천 * [**MPP 개념**](/ko/explanation/mpp) — charge 및 subscription 인텐트를 포함한 더 넓은 표준을 읽어보세요. * [**에이전트 정산**](/ko/explanation/agent-settlement) — MPP 세션이 Stable의 에이전트 결제 레일에서 어디에 위치할지 확인하세요. * [**Stable에서 MPP 엔드포인트 구축하기**](/ko/how-to/build-mpp-endpoint) — 세션이 출시되기 전까지 오늘날 charge 인텐트를 사용하세요. ## Machine Payments Protocol (MPP) MPP(Machine Payments Protocol)는 HTTP 리소스를 요청하는 동일한 요청 내에서 결제할 수 있는 개방형 표준입니다. 이는 [x402](/ko/explanation/x402)를 새로운 결제 의도(charge, subscription, session), 멀티 레일 지원(스테이블코인, 카드, Lightning), 프로덕션 기능(멱등성, 본문 다이제스트 바인딩, 만료), 추가 전송 방식(MCP, WebSocket)으로 확장합니다. 이 프로토콜은 IETF 표준 트랙에 올라 있습니다. ### MPP와 x402 MPP 클라이언트는 하위 호환성을 갖습니다. MPP 클라이언트는 변경 없이 기존 x402 서버를 호출할 수 있습니다. 두 프로토콜이 다른 부분은 다음과 같습니다: | **항목** | **x402** | **MPP** | | :-------------- | :------------------------------------------------------------ | :------------------------------------------------------- | | 결제 의도 | 요청별 charge | Charge, subscription, session | | 레일 | 블록체인만 | 스테이블코인, 카드, Lightning, 커스텀 | | 프로덕션 기능 | 제한적 | 멱등성, 본문 다이제스트 바인딩, 만료 | | 전송 방식 | HTTP | HTTP, MCP/JSON-RPC, WebSocket | | 헤더 (클라이언트 ↔ 서버) | `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` | `WWW-Authenticate` / `Authorization` / `Payment-Receipt` | | 거버넌스 | 커뮤니티 프로토콜 | IETF 표준 트랙 | | 메서드 작성 | 재단 통제 | 무허가형 | ### 챌린지, 자격 증명, 영수증 MPP는 모든 유료 요청을 클라이언트와 리소스 서버 간의 3단계 교환으로 감쌉니다: 1. **챌린지.** 서버는 `402 Payment Required`와 지원되는 메서드, 금액, 만료를 명시하는 `WWW-Authenticate` 헤더로 응답합니다. 2. **자격 증명.** 클라이언트는 메서드를 선택하고, 결제 증명에 서명한 뒤, 직렬화된 자격 증명을 담은 `Authorization` 헤더와 함께 요청을 다시 제출합니다. 3. **영수증.** 서버는 자격 증명을 검증하고, 결제를 정산한 뒤, 정산 참조를 담은 `Payment-Receipt` 헤더와 함께 응답을 반환합니다. 챌린지는 본문 다이제스트를 통해 요청 본문에 암호학적으로 바인딩될 수 있으므로, 한 요청에 대해 서명된 자격 증명은 다른 요청에 재사용할 수 없습니다. ### 결제 의도 **Charge.** 단일 리소스에 대한 일회성 결제입니다. 자격 증명은 정확한 금액의 한 번의 전송을 승인합니다. **Subscription.** 단일 범위 자격 증명 하에서 반복되는 결제입니다. 자격 증명은 청구 기간에 걸쳐 반복 청구를 승인하며, 레일이 갱신 주기를 적용합니다. **Session.** 오프체인 바우처를 사용하는 종량제입니다. 클라이언트는 자금을 에스크로에 한 번 예치한 뒤, 각 요청에 대해 저렴한 오프체인 바우처에 서명합니다. 순 금액만 온체인에서 정산됩니다. 자세한 내용은 [MPP 세션](/ko/explanation/mpp-sessions)을 참고하세요. ### 전송 방식 MPP는 여러 전송 방식에 걸쳐 동일한 챌린지 / 자격 증명 / 영수증 교환을 정의합니다: * **HTTP.** 기본값. 위에 나열된 헤더를 사용합니다. * **MCP / JSON-RPC.** MCP 서버가 개별 툴 호출을 수익화할 수 있게 합니다. AI 클라이언트는 툴을 호출하기 전에 자격 증명에 서명합니다. * **WebSocket.** 대역 내 바우처 충전이 가능한 지속 연결로, 스트리밍 세션을 위해 설계되었습니다. ### Stable에서의 MPP MPP는 Stable 결제 메서드를 기본 제공하지 않습니다. `mppx` SDK([wevm/mppx](https://github.com/wevm/mppx))에는 Tempo와 Stripe 메서드가 포함되어 있으며, `mpp.dev`는 Tempo, Stripe, Lightning, Solana, Stellar, Monad, RedotPay를 나열합니다. 현재 Stable은 두 목록 어디에도 없습니다. 이 표준은 무허가형이므로 직접 메서드를 작성할 수 있습니다. Stable의 USDT0의 경우, `verify()` 훅은 ERC-3009에 대한 서명 검증이며, 정산은 기존 정산 서비스에 위임됩니다. [Semantic Pay](https://docs.semanticpay.io)나 [Heurist](https://docs.heurist.ai/x402-products/facilitator) 같은 x402 퍼실리테이터, 또는 Stable 자체 [Gas Waiver](/ko/how-to/integrate-gas-waiver)가 그 예입니다. 전체 안내는 [Stable에서 MPP 엔드포인트 구축하기](/ko/how-to/build-mpp-endpoint)를 참고하세요. ### 다음 추천 * [**Stable에서 MPP 엔드포인트 구축하기**](/ko/how-to/build-mpp-endpoint) — USDT0를 위한 세 가지 MPP 커스텀 메서드 훅을 작성하고 실제 결제를 정산합니다. * [**MPP 세션**](/ko/explanation/mpp-sessions) — 오프체인 바우처와 단일 순 정산으로 마이크로페이먼트를 스트리밍합니다. * [**x402**](/ko/explanation/x402) — MPP가 일반화한 원래의 HTTP-402 프로토콜을 읽어봅니다. ## 개요 Stable은 USDT0가 네이티브 가스 토큰인 Layer 1이며, 표준 EVM 도구(Solidity, Foundry, Hardhat, ethers, viem, 그리고 `eth_*` JSON-RPC 메서드)가 변경 없이 작동합니다. RPC를 Stable로 설정하고 체인 ID를 확인하세요: ```text 988 ``` 다음 명령을 테스트하려면 [foundry](https://www.getfoundry.sh/)가 설치되어 있는지 확인하세요: ```bash cast chain-id --rpc-url https://rpc.stable.xyz ``` 전체 엔드포인트 목록(메인넷 및 테스트넷)은 [연결](/ko/reference/connect)을 참조하세요. ### 다음에 읽을 내용 아직 Stable에서 트랜잭션을 보내지 않았다면, 테스트넷에서 빠르게 따라 해볼 수 있는 [빠른 시작](/ko/tutorial/quick-start)부터 시작하세요. 그런 다음 빌드하려는 내용에 맞는 경로를 선택하세요: * 지갑, 위임, 에이전트 계정 → [계정](/ko/explanation/accounts-overview). * USDT0 이동 또는 결제 흐름 구축 → [결제](/ko/explanation/payments-overview). * 스마트 컨트랙트 배포 → [컨트랙트](/ko/explanation/contracts-overview). * AI 에디터 연동 또는 에이전트 유료 서비스 구축 → [에이전트 정산](/ko/explanation/agent-settlement). * 풀 노드 또는 아카이브 노드 운영, 생태계 제공자, 가스 대납 → [인프라](/ko/explanation/integrate-overview). 배포하기 전에, [핵심 개념](/ko/explanation/core-concepts)에서 Ethereum과 다른 네 가지 동작(USDT0 이중 역할, 보장된 블록 공간, 전송 애그리게이터, EVM 최종성)을 다룹니다. [프로덕션 준비](/ko/how-to/production-readiness)는 메인넷 준비 체크리스트입니다. ## 활용 사례 개요 Stable은 간단한 지갑 간 송금부터 에이전트 기반 서비스 결제까지 다양한 결제 패턴을 지원합니다. 아래 활용 사례는 오늘날 프로덕션에 바로 사용할 수 있는 패턴을 다룹니다. 곧 도입될 패턴(보장된 정산, 비공개 결제, 에이전트 간 상거래)에 대해서는 [예정된 활용 사례](/ko/explanation/upcoming-use-cases)를 참조하세요. ### 사용 가능한 활용 사례 * [**P2P 결제**](/ko/reference/p2p-payments) — 지갑 간 USDT0 송금. 1초 미만의 정산, Gas Waiver를 통한 가스 비용 제로. * [**구독 청구**](/ko/reference/subscriptions) — EIP-7702를 통한 풀 기반 정기 청구. 구독자가 한 번 승인하면 제공자가 주기마다 수금합니다. * [**인보이스 정산**](/ko/reference/invoices) — 결정론적 논스를 사용하는 B2B 인보이스 결제. 온체인 정산이 인보이스에 자동으로 연결됩니다. * [**호출당 결제 API**](/ko/reference/pay-per-call) — x402 미들웨어를 통한 요청별 HTTP 결제. 계정, API 키, 청구 주기가 필요 없습니다. ### 공통 기반 대부분의 패턴은 동일한 두 가지 프로토콜을 기반으로 구축됩니다: * **[ERC-3009](/ko/explanation/erc-3009)**: 위임 정산을 위한 서명된 승인. 인보이스, 호출당 결제, P2P 애플리케이션 시작 송금에 사용됩니다. * **[x402](/ko/explanation/x402)**: 표준 헤더를 통한 HTTP 네이티브 결제. 호출당 결제 API와 MCP 기반 결제 흐름에 사용됩니다. * **[EIP-7702](/ko/explanation/eip-7702)**: 정기 승인을 위한 EOA 위임. 구독 청구에 사용됩니다. ### 다음 추천 * [**ERC-3009**](/ko/explanation/erc-3009) — 핵심 정산 표준부터 시작하세요. * [**예정된 활용 사례**](/ko/explanation/upcoming-use-cases) — 에이전트 간 상거래, 보장된 정산, 비공개 결제를 미리 살펴보세요. ## 결제 가이드 결제 탭의 모든 가이드, 개념, 참조를 하려는 작업별로 그룹화했습니다. ### 보내기 및 전송 * [**첫 USDT0 전송하기**](/ko/tutorial/send-usdt0) — 동일한 잔액에서의 네이티브 및 ERC-20 전송. * [**제로 가스 트랜잭션**](/ko/how-to/zero-gas-transactions) — Gas Waiver로 수수료가 충당되는 USDT0 전송. * [**USDT0를 가스로 사용하기**](/ko/how-to/work-with-usdt-gas) — 트랜잭션을 올바르게 구성하기: priority tip 0, USDT0의 `value`. * [**USDT0를 Stable로 브리징하기**](/ko/tutorial/bridge-usdt0) — LayerZero OFT를 사용하여 Ethereum Sepolia에서 브리징. ### 결제 흐름 구축 * [**P2P 결제 알아보기**](/ko/how-to/build-p2p-payments) — 하나의 앱에서 지갑 + 보내기 + 받기 + 내역. * [**구독 및 수금**](/ko/how-to/subscribe-and-collect) — EIP-7702를 통한 풀 기반 정기 청구. * [**인보이스로 결제하기**](/ko/how-to/pay-with-invoice) — 인보이스 정산을 위한 결정적 nonce가 포함된 ERC-3009. * [**호출당 결제 API 구축하기**](/ko/how-to/build-pay-per-call) — x402 미들웨어로 HTTP 엔드포인트 수익화. ### 프로토콜 및 참조 * [**ERC-3009**](/ko/explanation/erc-3009) — Transfer With Authorization: 서명 기반 정산 프리미티브. * [**x402 (HTTP 네이티브 결제)**](/ko/explanation/x402) — 서버가 402로 응답하고, 클라이언트가 서명하며, 퍼실리테이터가 온체인에서 정산. * [**P2P 결제 참조**](/ko/reference/p2p-payments) — 모델 개요 및 전통적인 결제망과의 비교. * [**구독 참조**](/ko/reference/subscriptions) — 풀 기반 청구 모델 및 트레이드오프. * [**인보이스 참조**](/ko/reference/invoices) — 결정적 nonce 정산 모델. * [**호출당 결제 참조**](/ko/reference/pay-per-call) — x402 가격 책정 및 엔드포인트 디스커버리 모델. * [**예정된 사용 사례**](/ko/explanation/upcoming-use-cases) — 보장된 정산, 기밀 결제, 에이전트 간 결제. ## Stable의 결제 Stable은 결제를 중심으로 설계되었습니다. USDT0는 네이티브 자산이자 가스 토큰이므로, 정산과 수수료가 하나의 잔액을 공유합니다. 단일 슬롯 완결성(single-slot finality)은 송금이 1초 이내에 처리됨을 의미합니다. ERC-3009, EIP-7702, x402는 우회책이 아니라 네이티브 프리미티브입니다 — 서명으로 정산하거나, 위임된 계정에서 자금을 끌어오거나, 빌링 스택을 운영하지 않고도 HTTP 요청당 과금할 수 있습니다. ### 무엇을 구축할 수 있나요 * **P2P 송금** — 21k 가스와 1초 미만의 완결성을 갖춘 네이티브 USDT0 전송. * **구독** — EIP-7702 위임을 이용한 풀(pull) 기반 반복 빌링. * **인보이스 정산** — 정확한 대사를 위한 결정적 nonce를 갖춘 ERC-3009 `transferWithAuthorization`. * **호출당 과금 API** — 요청당 USDT0 결제를 위한 x402 미들웨어; API 키나 가입이 필요 없습니다. * **제로 가스 UX** — Gas Waiver 서비스를 통한 애플리케이션 후원 트랜잭션. * **크로스체인 USDT0** — Ethereum 및 기타 네트워크로부터의 LayerZero OFT 브리징. ### Stable의 차별점 * **모든 것을 위한 하나의 자산**: 송신자가 별도의 가스 토큰을 보유할 필요가 없습니다. * **네이티브 ERC-3009**: USDT0가 `transferWithAuthorization`을 직접 구현하므로, 결제가 서명으로 정산되며 approve 단계가 필요 없습니다. * **결정적 완결성**: 블록은 커밋되는 순간 최종 확정됩니다. 확인 대기가 없습니다. * **네이티브 x402**: 퍼실리테이터가 Gas Waiver를 통해 가스를 지불하지 않으므로, 요청당 정산 비용이 1센트 미만으로 유지됩니다. ### 여기서 시작하세요 * [**첫 USDT0 보내기**](/ko/tutorial/send-usdt0) — 동일한 잔액에서의 네이티브 및 ERC-20 전송. * [**제로 가스 트랜잭션**](/ko/how-to/zero-gas-transactions) — Gas Waiver로 수수료가 충당된 USDT0 전송. * [**P2P 결제 배우기**](/ko/how-to/build-p2p-payments) — 지갑 + 보내기 + 받기 + 내역 앱을 처음부터 구축. * [**호출당 과금 API 구축**](/ko/how-to/build-pay-per-call) — x402로 HTTP 엔드포인트를 요청당 과금. * [**Stable SDK**](/ko/explanation/sdk-overview) — 타입이 지정된 클라이언트로 몇 줄 만에 transfer, bridge, swap을 수행. ### 결제 프리미티브 * [**ERC-3009**](/ko/explanation/erc-3009) — Transfer With Authorization: 인보이스와 x402 뒤에 있는 정산 표준. * [**x402 (HTTP 네이티브 결제)**](/ko/explanation/x402) — 서버가 402로 응답하고, 클라이언트가 ERC-3009에 서명하면, 퍼실리테이터가 온체인에서 정산합니다. ### 다음 추천 * [**결제 가이드 색인**](/ko/explanation/payments-guides) — 결제(Payments) 탭 아래의 모든 가이드, 개념, 레퍼런스. * [**구독 및 수금**](/ko/how-to/subscribe-and-collect) — EIP-7702를 통한 풀 기반 반복 빌링. * [**인보이스로 결제하기**](/ko/how-to/pay-with-invoice) — 정확한 대사를 위한 결정적 nonce를 갖춘 ERC-3009. ## Stable SDK `@stablechain/sdk`는 Stable을 위한 공식 TypeScript 클라이언트입니다. viem을 감싸서 가장 자주 사용하는 작업들을 위한 작고 타입이 지정된 API를 제공합니다: USDT0 전송, 체인 간 브리지, 그리고 Stable에서의 토큰 스왑. 라우팅, 승인, 소수점 처리, 체인 전환은 모두 자동으로 처리됩니다. ```ts import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const stable = createStable({ network: Network.Mainnet, account: privateKeyToAccount("0x..."), }); const { txHash } = await stable.transfer({ from: "0xYourAddress", to: "0xRecipient", amount: 10, }); ``` ```text txHash: 0x8f3a...2d41 ``` ### SDK가 하는 일 * **`transfer`** — 네이티브 USDT0 또는 Stable의 모든 ERC-20을 전송합니다. 가스는 자동으로 USDT0로 지불됩니다. * **`quoteBridge` / `bridge`** — 체인 간 전송. USDT0 → USDT0에는 LayerZero를, 그 외 모든 것에는 LI.FI를 사용합니다. 경로는 자동으로 선택됩니다. * **`quoteSwap` / `swap`** — LI.FI를 통한 동일 체인 토큰 스왑이며, ERC-20 승인은 내부적으로 처리됩니다. SDK는 npm에 [`@stablechain/sdk`](https://www.npmjs.com/package/@stablechain/sdk)로 게시되어 있으며, 피어 의존성으로 `viem >= 2.0.0`이 필요합니다. ### 언제 사용해야 하나 (그리고 언제 사용하지 말아야 하나) 라우팅과 승인 보일러플레이트를 숨겨주는, 타입이 지정되고 의견이 반영된 클라이언트를 원할 때 SDK를 사용하세요. 트랜잭션 구성에 대한 직접적인 제어, 사용자 정의 가스 전략, 또는 transfer / bridge / swap 외부의 컨트랙트 호출이 필요할 때는 원시 viem이나 ethers로 내려가세요. :::note SDK는 viem 호환 서명자라면 무엇으로든 서명합니다: 프라이빗 키 `Account`, `custom(window.ethereum)`과 같은 브라우저 `Transport`, 또는 미리 빌드된 `WalletClient`(예를 들어 wagmi의 `useWalletClient`가 반환하는 것). ::: ### 여기서 시작하세요 * [**빠른 시작**](/ko/tutorial/sdk-quickstart) — SDK를 설치하고 테스트넷에서 첫 번째 전송, 브리지, 스왑을 실행해 보세요. * [**SDK 레퍼런스**](/ko/reference/sdk) — 모든 메서드, 설정 옵션, 열거형, 그리고 오류 클래스. * [**viem과 함께 사용하기**](/ko/how-to/sdk-with-viem) — 서버 측 계정, 브라우저 지갑, 그리고 직접 만든 `WalletClient` 사용. * [**wagmi와 함께 사용하기**](/ko/how-to/sdk-with-wagmi) — `useWalletClient`와 훅으로 SDK를 React 앱에 연결하기. ### 다음 권장 사항 * [**npm에서 설치하기**](https://www.npmjs.com/package/@stablechain/sdk) — npmjs.com에서 패키지를 보고 최신 버전을 확인하세요. * [**Stable에 연결하기**](/ko/reference/connect) — 메인넷과 테스트넷의 체인 ID, RPC 엔드포인트, 그리고 익스플로러. * [**테스트넷 지갑에 자금 충전하기**](/ko/how-to/use-faucet) — 빠른 시작을 실행하기 전에 faucet에서 테스트넷 USDT0를 받으세요. ## StableDB 블록체인 성능에서 주요 병목 중 하나는 **디스크 I/O**에 있습니다. 보다 구체적으로는, 블록 실행 후 상태 데이터를 커밋하고 저장하는 작업이 핵심 병목 지점입니다. Stable은 이 문제를 해결하기 위해 `MemDB`, `VersionDB`, 메모리 매핑 파일 I/O 메커니즘 (`mmap`)와 같은 아키텍처 혁신을 도입하여 처리량을 획기적으로 향상시킵니다. ### 디스크 I/O가 병목이 되는 이유 #### 상태 전환과 저장 트랜잭션 블록이 실행될 때마다 블록체인은 하나의 상태에서 다음 상태로 전환됩니다. 이 과정은 다음과 같은 두 가지 기본 단계로 나뉩니다: 1. **상태 커밋**: 트랜잭션 실행 후, 새로운 애플리케이션 상태가 커밋됩니다 2. **상태 저장**: 커밋된 상태가 디스크에 영구적으로 저장되어, 향후 접근성과 과거 기록에 검증을 가능하게 합니다. Coupled State Commitment and Storage 기존 아키텍처에서는, 상태 저장이 상태 커밋과 **강하게 결합되어** 있습니다. 이는 다음을 의미합니다. * 노드는 다음 블록을 실행하기 전에 새로운 상태가 디스크에 완전히 저장될 때까지 대기해야 합니다. * 상태 데이터는 고정된 주소를 가지지 않은 임의의 디스크 위치에 기록됩니다. 이로 인해 이후 트랜잭션 실행 시 상태 데이터를 검색하는 데 높은 레이턴시가 발생합니다. 합의 및 실행 계층이 아무리 최적화되어 있어도, 이처럼 느린 디스크 작업에 대한 의존성 때문에 시스템 전체 성능에는 상한선이 생깁니다. ### 더 높은 처리량을 위한 DB 연산 최적화 이러한 제약을 극복하기 위해 Stable은 **상태 연산의 분리** 및 **메모리 매핑 DB 최적화** 라는 두 가지 아키텍처 개선을 제안합니다. #### 1. 상태 커밋과 저장의 분리 Decoupled State Commitment and Storage 첫 번째 단계는 상태 커밋과 저장을 분리하는 것입니다: * 새로운 상태가 커밋되면, 노드는 즉시 다음 블록을 실행합니다. * 상태를 디스크에 저장하는 작업은 비동기적으로 백그라운드에서 처리됩니다. 이러한 분리를 통해 실행은 디스크 쓰기의 레이턴시에 영향받지 않고 즉시 진행될 수 있으며, 의존성에 의해 작업이 멈추는 현상을 제거하여 결국 전체적인 성능이 향상될 수 있습니다. #### 2. `mmap` 을 통한`MemDB` 및 `VersionDB` 도입 Stable은 메모리 매핑 파일 I/O 메커니즘 (`mmap`)을 기반으로 한 이중 데이터베이스 모델을 도입하여 이를 보강합니다: * **MemDB (메모리 DB)**: * 자주 접근되는 최근/활성 데이터를 저장 * mmap을 통한 고정 주소 매핑을 사용하여 빠르고 결정론적인 조회가 가능 * 최근 수정된 상태에 접근하는 대부분의 트랜잭션에 대해 이상적인 구조 * **VersionDB (히스토리컬 DB)**: * 오래된 과거 상태를 디스크에 저장 * 자주 접근되지 않는, 오래되거나 넓은 범위의 데이터에 대한 쿼리에 최적화됨 이 설계를 통해 **핫 데이터는 빠른 메모리 기반 구조에서 제공**되고, 콜드 데이터는 느리지만 영구적인 스토리지로 오프로드됩니다. 효율을 고려하여 상태를 분류하고 `mmap` 접근을 활용함으로써, Stable은 블록 실행 중 DB의 읽기/쓰기 지연을 크게 줄일 수 있습니다. ### 예상되는 성능 개선 및 선례 이 아키텍처 최적화는 이론적인 아이디어에 그치지 않습니다. Sei 및 Cronos와 같은 고성능 블록체인들이 유사한 구조를 이미 구현하고 있으며, 메모리 매핑 DB 및 상태 커밋/저장의 분리를 통해 **전체 TPS가 최대 2배 향상**되었음을 보고했습니다. Stable 또한 이와 유사한 성능 향상을 기대하고 있으며, 새로운 아키텍처에서는 스토리지 레이어가 더 이상 병목이 되지 않기 때문에, 합의 및 실행 성능이 디스크 연산에 의해 제한되지 않고 자유롭게 확장될 수 있게 됩니다. ### 추가 자료 더 자세한 기술 분석과 구현 내용을 보려면 다음 문서를 참고하세요: * [ADR-065: Cosmos Store V2 Architecture](https://docs.cosmos.network/main/build/architecture/adr-065-store-v2) * [MemIAVL: A Practical Guide](https://hackmd.io/@yihuang/rkeCvy5xh) * [Cronos MemIAVL Node Configuration](https://docs.cronos.org/for-node-hosts/running-nodes/memiavl) * [Sei’s DB Design Approach](https://4pillars.io/ko/articles/sei-db) ## Staking ### 개요 `staking` precompile 컨트랙트는 Stable SDK의 `x/staking` 모듈 기능을 EVM 환경에서 사용할 수 있도록 브리지 역할을 합니다. ### 목차 1. **[개념](#concepts)** 2. **[구성](#configuration)** 3. **[메서드](#methods)** 4. **[이벤트](#events)** ### 개념 Stable SDK의 `x/staking` 모듈에서는 스테이킹을 위해 체인 초기화 시 bond denom이 등록되어야 합니다. Validator와 delegator는 bond denom 스테이킹 토큰만 사용할 수 있습니다. `staking` precompile 컨트랙트에서는 validator 또는 delegator가 호출자인지 확인하는 추가 검증이 수행됩니다. ### 구성 컨트랙트 주소와 가스 비용은 사전에 정의되어 있습니다. #### 컨트랙트 주소 * `0x0000000000000000000000000000000000000800` ### 메서드 #### `createValidator` Validator가 생성됩니다. Validator는 운영자의 초기 delegation과 함께 생성되어야 합니다. 잠재적인 delegator를 위해 validator는 자신의 정보와 수수료율 계획을 제공해야 합니다. Delegator는 시장 메커니즘의 자연스러운 규제를 통해 공개된 정보를 바탕으로 자신의 토큰을 위임할 validator를 선택할 수 있습니다. Validator가 성공적으로 등록되면 `CreateValidator` 이벤트가 발생합니다. ##### Inputs | Name | Type | Description | | ----------------- | --------------- | --------------------------------- | | description | Description | validator의 정보 | | commissionRates | CommissionRates | validator가 보상받는 스테이킹 토큰의 수수료율 | | minSelfDelegation | uint256 | validator의 최소 자체 위임 금액 | | validatorAddress | address | validator의 주소 | | pubkey | string | validator의 공개 키 | | value | uint256 | validator에게 초기 자체 위임되는 스테이킹 토큰의 양 | `Description`은 다음 필드를 가진 구조체입니다: | Name | Type | Description | | --------------- | ------ | ------------------- | | moniker | string | validator의 이름 | | identity | string | validator의 신원 | | website | string | validator 웹사이트의 URL | | securityContact | string | 보안 연락처 정보 | | details | string | validator의 추가 설명 | `CommissionRates`는 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ------------- | ------- | ------------------------------- | | rate | uint256 | validator가 받는 현재 수수료율 | | maxRate | uint256 | 최대 수수료율 (이보다 높게 설정할 수 없음) | | maxChangeRate | uint256 | validator가 하루에 변경할 수 있는 최대 수수료율 | `rate`는 시장에서 수용 가능한 적절한 값으로 설정해야 합니다. * Validator의 수수료율이 높으면 delegator의 수익이 낮아집니다. * Validator의 수수료율이 낮으면 validator의 수익이 낮아지고 운영이 어려워집니다. 높은 `maxRate`는 validator의 예상치 못한 높은 수수료율에 대한 delegator의 우려를 야기할 수 있으므로 `maxRate`는 신중하게 설정해야 합니다. `maxChangeRate`는 초기화 후 변경할 수 없습니다. ##### Outputs | Name | Type | Description | | ------- | ---- | -------------------------- | | success | bool | validator가 성공적으로 등록되면 true | #### `editValidator` Validator가 정보를 업데이트합니다. Validator는 `CommissionRates` 구조체의 `maxRate` 및 `maxChangeRate`와 같이 변경 불가능한 필드를 제외한 정보만 업데이트할 수 있습니다. Validator가 성공적으로 업데이트되면 `EditValidator` 이벤트가 발생합니다. ##### Inputs | Name | Type | Description | | ----------------- | ----------- | ----------------------------- | | description | Description | validator의 정보 | | validatorAddress | address | validator의 주소 | | commissionRate | int256 | validator가 보상받는 스테이킹 토큰의 수수료율 | | minSelfDelegation | int256 | validator의 최소 자체 위임 금액 | ##### Outputs | Name | Type | Description | | ------- | ---- | ---------------------------- | | success | bool | validator가 성공적으로 업데이트되면 true | #### `delegate` Delegator가 validator에게 위임할 토큰의 양을 설정합니다. Delegation이 성공적으로 완료되면 `Delegate` 이벤트가 발생합니다. ##### Inputs | Name | Type | Description | | ---------------- | ------- | --------------------------- | | delegatorAddress | address | delegator의 주소 | | validatorAddress | address | validator의 주소 | | amount | uint256 | validator에게 위임되는 스테이킹 토큰의 양 | ##### Outputs | Name | Type | Description | | ------- | ---- | --------------------------- | | success | bool | delegation이 성공적으로 완료되면 true | ##### Events `newShares`는 delegator의 소유 비율을 나타냅니다. 동일한 양의 토큰이 위임되더라도 시간에 따라 계산되는 shares는 달라질 수 있습니다. #### `undelegate` Delegator가 validator에게 위임한 토큰의 양을 인출합니다. 언델리게이션이 성공적으로 완료되면 `Unbond` 이벤트가 발생합니다. ##### Inputs | Name | Type | Description | | ---------------- | ------- | --------------------------------- | | delegatorAddress | address | delegator의 주소 | | validatorAddress | address | validator의 주소 | | amount | uint256 | validator로부터 언델리게이트하려는 스테이킹 토큰의 양 | ##### Outputs | Name | Type | Description | | ------- | ---- | ----------------------- | | success | bool | 언델리게이션이 성공적으로 완료되면 true | #### `redelegate` Delegator가 validator에게 위임한 토큰의 양을 다른 validator에게 재위임합니다. 재위임이 성공적으로 완료되면 `Redelegate` 이벤트가 발생합니다. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ----------------- | | delegatorAddress | address | delegator의 주소 | | validatorSrc | string | 출발지 validator의 주소 | | validatorDst | string | 목적지 validator의 주소 | | amount | uint256 | 재위임할 스테이킹 토큰의 양 | ##### Outputs | Name | Type | Description | | ------- | ---- | -------------------- | | success | bool | 재위임이 성공적으로 완료되면 true | #### `delegation` Delegator와 validator 간의 delegation 정보를 반환합니다. Delegation을 찾을 수 없으면 `shares`와 `balance`는 `0`이 됩니다. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ------------- | | delegatorAddress | address | delegator의 주소 | | validatorAddress | address | validator의 주소 | ##### Outputs | Name | Type | Description | | ------- | ------- | ---------------- | | shares | uint256 | 위임된 shares | | balance | Coin | 위임된 토큰의 양과 denom | `Coin`은 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ------ | ------- | ----------- | | denom | string | 보상의 denom | | amount | uint256 | 보상의 양 | #### `unbondingDelegation` Delegator와 validator 간의 언본딩 delegation 정보를 반환합니다. 언본딩 delegation을 찾을 수 없으면 빈 `UnbondingDelegationOutput`이 반환됩니다. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ------------- | | delegatorAddress | address | delegator의 주소 | | validatorAddress | address | validator의 주소 | ##### Outputs | Name | Type | Description | | ------------------- | ------------------------- | ------------------ | | unbondingDelegation | UnbondingDelegationOutput | 언본딩 delegation의 정보 | `UnbondingDelegationOutput`은 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ---------------- | --------------------------- | ------------------- | | validatorAddress | address | validator의 주소 | | delegatorAddress | address | delegator의 주소 | | entries | UnbondingDelegationEntry\[] | 언본딩 delegation의 항목들 | `UnbondingDelegationEntry`는 다음 필드를 가진 구조체입니다: | Name | Type | Description | | -------------- | ------ | ----------- | | creationHeight | uint64 | 항목의 생성 높이 | | completionTime | uint64 | 항목의 완료 시간 | | initialBalance | Coin | 항목의 초기 잔액 | | balance | Coin | 항목의 잔액 | #### `validator` Validator 정보를 반환합니다. Validator를 찾을 수 없으면 빈 `ValidatorOutput`이 반환됩니다. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ------------- | | validatorAddress | address | validator의 주소 | ##### Outputs | Name | Type | Description | | --------- | --------- | ------------- | | validator | Validator | validator의 정보 | `Validator`는 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ----------------- | ------- | ----------------------------- | | operatorAddress | address | validator의 주소 | | consensusPubkey | string | validator의 공개 키 | | jailed | bool | validator가 jailed 상태인지 여부 | | status | int32 | validator의 상태 | | tokens | uint256 | validator에게 위임된 스테이킹 토큰의 양 | | delegatorShares | uint256 | delegation shares의 양 | | description | string | validator의 설명 | | unbondingHeight | int64 | validator가 언본딩 중인 높이 | | unbondingTime | int64 | validator가 언본딩 중인 시간 | | commission | uint256 | validator가 보상받는 스테이킹 토큰의 수수료율 | | minSelfDelegation | uint256 | validator의 최소 자체 위임 금액 | #### `validators` 상태와 일치하는 모든 validator를 반환합니다. Validator를 찾을 수 없으면 빈 `ValidatorsOutput`이 반환됩니다. `x/staking` 모듈에서 선언된 상태는 다음 중 하나일 수 있습니다: * 0 : "BOND\_STATUS\_UNSPECIFIED", 지정되지 않은 상태 * 1 : "BOND\_STATUS\_UNBONDING", validator가 언본딩 중 * 2 : "BOND\_STATUS\_UNBONDED", validator가 언본딩됨 * 3 : "BOND\_STATUS\_BONDED", validator가 본딩됨 ##### Inputs | Name | Type | Description | | ----------- | ------- | ------------- | | status | string | validator의 상태 | | pageRequest | PageReq | 페이지네이션 요청 | `PageReq`는 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ---------- | ----- | -------------------- | | key | bytes | 페이지의 키 | | offset | int64 | 페이지의 오프셋 | | limit | int64 | 페이지의 제한 | | countTotal | bool | 결과의 총 개수를 세어야 하는지 여부 | | reverse | bool | 결과를 역순으로 정렬할지 여부 | ##### Outputs | Name | Type | Description | | ------------ | ------------ | -------------- | | validators | Validator\[] | validator들의 배열 | | pageResponse | PageResp | 페이지네이션 응답 | `PageResp`는 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ------- | ------ | ----------- | | nextKey | bytes | 다음 페이지의 키 | | total | uint64 | 결과의 총 개수 | #### `redelegation` Delegator, 출발지 validator, 목적지 validator의 재위임 정보를 반환합니다. 재위임을 찾을 수 없으면 빈 `RedelegationOutput`이 반환됩니다. ##### Inputs | Name | Type | Description | | ------------------- | ------- | ----------------- | | delegatorAddress | address | delegator의 주소 | | srcValidatorAddress | address | 출발지 validator의 주소 | | dstValidatorAddress | address | 목적지 validator의 주소 | ##### Outputs | Name | Type | Description | | ------------ | ------------------ | ----------- | | redelegation | RedelegationOutput | 재위임의 정보 | `RedelegationOutput`은 다음 필드를 가진 구조체입니다: | Name | Type | Description | | ------------------- | -------------------- | ----------------- | | delegatorAddress | address | delegator의 주소 | | validatorSrcAddress | address | 출발지 validator의 주소 | | validatorDstAddress | address | 목적지 validator의 주소 | | entries | RedelegationEntry\[] | 재위임의 항목들 | `RedelegationEntry`는 다음 필드를 가진 구조체입니다: | Name | Type | Description | | -------------- | ------ | ----------- | | creationHeight | uint64 | 항목의 생성 높이 | | completionTime | uint64 | 항목의 완료 시간 | | initialBalance | Coin | 항목의 초기 잔액 | | balance | Coin | 항목의 잔액 | #### `redelegations` Delegator, 출발지 validator, 목적지 validator의 모든 재위임을 반환합니다. 재위임을 찾을 수 없으면 빈 `RedelegationResponse`와 `PageResp`가 반환됩니다. ##### Inputs | Name | Type | Description | | ------------------- | ------- | ----------------- | | delegatorAddress | address | delegator의 주소 | | srcValidatorAddress | address | 출발지 validator의 주소 | | dstValidatorAddress | address | 목적지 validator의 주소 | | pageRequest | PageReq | 페이지네이션 요청 | ##### Outputs | Name | Type | Description | | ------------ | ----------------------- | ----------- | | response | RedelegationResponse\[] | 재위임들의 정보 | | pageResponse | PageResp | 페이지네이션 응답 | ### 이벤트 #### CreateValidator | Name | Type | Indexed | Description | | -------- | ------- | ------- | --------------------------------- | | valiAddr | address | Y | validator의 주소 | | value | uint256 | N | validator에게 초기 자체 위임되는 스테이킹 토큰의 양 | #### EditValidator | Name | Type | Indexed | Description | | ----------------- | ------- | ------- | ----------------------------------- | | valiAddr | address | Y | validator의 주소 | | commissionRate | int256 | N | validator가 보상받는 스테이킹 토큰의 업데이트된 수수료율 | | minSelfDelegation | int256 | N | validator의 업데이트된 최소 자체 위임 금액 | #### Delegate | Name | Type | Indexed | Description | | ------------- | ------- | ------- | ----------------------------------- | | delegatorAddr | address | Y | delegator의 주소 | | validatorAddr | string | Y | validator의 주소 | | amount | uint256 | N | validator에게 위임되는 스테이킹 토큰의 양 | | newShares | uint256 | N | delegation 이후의 delegation shares의 양 | #### Unbond | Name | Type | Indexed | Description | | -------------- | ------- | ------- | ------------------------------- | | delegatorAddr | address | Y | delegator의 주소 | | validatorAddr | string | Y | validator의 주소 | | amount | uint256 | N | validator로부터 언델리게이트된 스테이킹 토큰의 양 | | completionTime | uint256 | N | 언델리게이션의 완료 시간 | #### Redelegate | Name | Type | Indexed | Description | | ------------------- | ------- | ------- | ----------------- | | delegatorAddr | address | Y | delegator의 주소 | | validatorSrcAddress | address | Y | 출발지 validator의 주소 | | validatorDstAddress | address | Y | 목적지 validator의 주소 | | amount | uint256 | N | 재위임할 스테이킹 토큰의 양 | | completionTime | uint256 | N | 재위임의 완료 시간 | ## 시스템 모듈 Stable의 핵심 프로토콜 동작은 SDK 모듈인 `x/bank`, `x/distribution`, `x/staking`에 존재합니다. 이 동작을 EVM에서 접근 가능하게 하기 위해, Stable은 각 모듈을 고정된 주소의 \*\*사전 컴파일된 컨트랙트(precompiled contract)\*\*로 노출합니다. Solidity로 작성된 컨트랙트는 precompile을 직접 호출하고, EVM은 그 호출을 네이티브 SDK 핸들러로 라우팅합니다. Precompile은 프로토콜 수준에서 구현되므로 동등한 Solidity 재구현보다 훨씬 가스 효율적입니다. ### 세 가지 모듈 | 모듈 | Precompile 주소 | 목적 | | :--------------------------------------------------------- | :--------------------- | :------------------------------------------------------ | | [Bank](/ko/explanation/bank-module) | `0x0000…1003` (STABLE) | 토큰 전송, 잔액 회계, allowance 관리, 권한이 부여된 컨트랙트를 위한 mint/burn. | | [Distribution](/ko/explanation/distribution-module) | `0x0000…0801` | 스테이킹 보상 청구, 보상 조회, 출금 주소 관리. | | [Staking](/ko/explanation/staking-module) | `0x0000…0800` | 위임, 위임 해제, 재위임, 검증자 조회. | | [System transactions](/ko/explanation/system-transactions) | `0x0000…9999` | SDK 계층 작업(예: 언본딩 완료)을 위한 프로토콜에서 발생하는 EVM 이벤트. | 위의 각 페이지는 해당 모듈이 무엇을 하는지, 언제 사용하는지, 그리고 ABI를 어디에서 찾을 수 있는지 설명합니다. ### 왜 Solidity가 아닌 precompile인가 두 가지 이유가 있습니다: * **가스 효율성.** Precompile은 프로토콜의 네이티브 실행 경로에서 실행됩니다. 동등한 Solidity 컨트랙트는 동일한 로직을 훨씬 높은 가스 비용으로 재구현하게 됩니다. * **단일 진실 공급원(Single source of truth).** Staking, distribution, 토큰 공급량은 프로토콜 수준의 상태입니다. 이를 precompile을 통해 노출하면 SDK와 어긋날 수 있는 중복된 Solidity 구현을 유지할 필요가 없어집니다. ### 권한 부여 일부 precompile 메서드(`mint`, `burn`, 프로토콜 수준의 스테이킹 작업)는 호출자 권한 부여가 필요합니다. `x/precompile` 모듈은 온체인 화이트리스트를 유지하며, 등록되지 않은 컨트랙트의 호출은 revert됩니다. 이는 읽기/전송 메서드의 일반적인 EVM 사용을 막지 않으면서 권한이 필요한 작업을 거버넌스로 통제합니다. ### 다음 권장 사항 * [**Bank 모듈**](/ko/explanation/bank-module) — 토큰 전송, allowance, mint/burn 권한 부여 모델을 이해합니다. * [**Staking 모듈**](/ko/explanation/staking-module) — 위임과 검증자 관리가 어떻게 EVM에 도달하는지 확인합니다. * [**System transactions**](/ko/explanation/system-transactions) — 언본딩 완료와 같은 프로토콜 수준의 이벤트가 어떻게 EVM 로그로 표면화되는지 알아봅니다. ## System Transactions ### 요약 시스템 트랜잭션은 Stable 프로토콜이 Stable SDK 작업에 대한 EVM 이벤트를 발생시킬 수 있는 방법을 제공합니다. 언본딩 완료와 같은 스테이킹 이벤트가 SDK 계층에서 발생하면 프로토콜은 해당 이벤트를 발생시키는 EVM 트랜잭션을 자동으로 생성하여 이러한 작업이 EVM 도구 및 애플리케이션에 완전히 표시되도록 합니다. ### 동기 Stable의 EVM 사용자와 애플리케이션은 `eth_getLogs`와 같은 표준 EVM 인터페이스를 통해 블록체인 이벤트를 모니터링할 것으로 기대합니다. 그러나 중요한 작업은 EVM 이벤트를 자연스럽게 발생시키지 않는 Stable SDK 모듈에서 발생합니다. 이는 가시성 격차를 만듭니다: EVM dapp은 사용자의 토큰이 언제 언본딩을 완료하는지 쉽게 추적할 수 없습니다. 시스템 트랜잭션은 이 격차를 해소합니다. 스테이킹 모듈이 언본딩 작업을 완료하면 Stable의 x/stable 모듈이 이벤트를 감지하고 StableSystem 프리컴파일 (`0x0000000000000000000000000000000000009999`)을 호출하는 시스템 트랜잭션을 생성합니다. 그런 다음 프리컴파일은 모든 dapp이 구독할 수 있는 적절한 EVM 이벤트를 발생시킵니다. 시스템 트랜잭션은 프로토콜만 사용할 수 있는 특수 발신자 주소(`0x8888888888888888888888888888888888888888`)로 실행됩니다. 이는 누구도 프로토콜 이벤트를 위조할 수 없도록 방지하면서 이벤트 발생을 무신뢰적이고 온체인에서 검증 가능하게 유지합니다. ### 명세 시스템 트랜잭션은 세 가지 주요 구성 요소를 통해 작동합니다: x/stable 모듈의 EndBlocker, PrepareProposal 핸들러 및 StableSystem 프리컴파일입니다. #### 아키텍처 개요 system-transaction-architecture #### StableSystem 프리컴파일 StableSystem 프리컴파일은 `0x0000000000000000000000000000000000009999`에 있으며 EVM 이벤트를 발생시켜야 하는 프로토콜 수준 작업을 처리합니다. 현재 언본딩 완료 알림을 지원합니다. ```solidity interface IStableSystem { /// @notice 대기 중인 언본딩 완료를 처리하고 EVM 이벤트를 발생시킵니다 /// @param blockHeight 완료를 처리할 블록 높이 /// @dev 시스템 트랜잭션에서만 호출 가능 (from = 0x8888888888888888888888888888888888888888) /// @dev 호출당 최대 100개의 완료를 처리 /// @dev 처리된 완료를 큐에서 자동으로 삭제 function notifyUnbondingCompletions(int64 blockHeight) external; /// @notice 언본딩 작업이 완료될 때 발생 /// @param delegator 토큰을 위임한 주소 /// @param validator 토큰이 위임된 검증자 주소 /// @param amount 언본딩을 완료한 토큰 양 (uusdc 단위) event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); /// @notice 호출자가 권한이 없음 (시스템 트랜잭션 발신자가 아님) error Unauthorized(); } ``` #### 시스템 트랜잭션 발신자 시스템 트랜잭션은 `0x8888888888888888888888888888888888888888`을 발신자 주소로 사용합니다. 이 주소는: * 서명 검증이 필요 없음 * PrepareProposal에서 생성된 트랜잭션만 사용 가능 * 사용자나 컨트랙트가 위조할 수 없음 * SystemTxDecorator ante 핸들러를 통해 수수료 공제를 건너뜀 EVM은 `msg.sender == 0x8888888888888888888888888888888888888888`을 확인하여 시스템 트랜잭션을 인식합니다. 프리컴파일은 이를 사용하여 프로토콜 전용 작업을 제한할 수 있습니다. #### 이벤트 기반 흐름 사용자의 언본딩 기간이 완료되면 다음과 같은 일이 발생합니다: 1. **Stable SDK 계층:** 스테이킹 모듈의 EndBlocker가 언본딩을 완료하고 위임자 주소, 검증자 주소 및 금액과 함께 EventTypeCompleteUnbonding을 발생시킵니다. 2. **감지:** x/stable 모듈의 EndBlocker는 스테이킹 이후에 실행되며 블록의 이벤트 로그에서 언본딩 이벤트를 스캔합니다. 각 완료에 대해 위임자 주소, 검증자 주소, 금액 및 블록 높이를 포함하는 항목을 상태에 큐에 넣습니다. 3. **시스템 트랜잭션 생성**: 다음 블록의 PrepareProposal에서 앱은 대기 중인 모든 완료를 쿼리합니다. 있는 경우 현재 블록 높이로 StableSystem.notifyUnbondingCompletions(blockHeight)를 호출하는 시스템 트랜잭션을 생성합니다. 이 트랜잭션은 사용자 트랜잭션보다 먼저 블록 앞에 배치됩니다. 4. **실행:** 블록 실행 중에 시스템 트랜잭션이 먼저 실행됩니다. 프리컴파일은 해당 블록 높이에서 대기 중인 완료에 대한 상태를 쿼리하고, 각 완료에 대해 UnbondingCompleted 이벤트를 발생시키며(최대 100개), 큐에서 삭제합니다. 5. **EVM 가시성:** 이벤트는 트랜잭션 영수증과 로그에 나타나며, eth\_getLogs 쿼리, 블록 탐색기 및 StableSystem 프리컴파일을 모니터링하는 모든 애플리케이션에 표시됩니다. #### 배치 처리 블록이 너무 커지는 것을 방지하기 위해 시스템은 블록당 최대 100개의 언본딩 완료를 처리합니다. 150개의 완료가 큐에 들어가면: * 블록 N: 완료 0-99를 처리하는 시스템 트랜잭션 생성 * 블록 N+1: 완료 100-149를 처리하는 시스템 트랜잭션 생성 프리컴파일은 calldata에서 완료 데이터를 받는 대신 상태를 직접 쿼리합니다. 이렇게 하면 트랜잭션 크기를 예측 가능하게 유지하고 데이터를 비싼 calldata에서 더 저렴한 상태 읽기로 이동합니다. ### 사용 예시 가장 일반적인 사용 사례는 언본딩 기간이 완료될 때 사용자에게 알려야 하는 스테이킹 대시보드입니다. 다음은 언본딩 완료 리스너를 설정하는 방법입니다. ```javascript import { ethers } from 'ethers'; // StableSystem 프리컴파일 주소 const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999'; // UnbondingCompleted 이벤트의 ABI const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; // Stable 네트워크에 연결 const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz'); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); // 모든 언본딩 완료 구독 stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => { console.log('언본딩 완료!'); console.log('위임자:', delegator); console.log('검증자:', validator); console.log('금액:', ethers.formatEther(amount), '토큰'); console.log('블록:', event.log.blockNumber); console.log('트랜잭션 해시:', event.log.transactionHash); }); ``` 이 리스너는 사용자의 언본딩이 완료될 때마다 실행됩니다. 프로덕션 dApp의 경우 일반적으로 특정 사용자에 대한 이벤트를 필터링합니다. #### 특정 사용자에 대한 이벤트 필터링 특정 위임자 주소에 대한 이벤트만 받으려면 인덱싱된 이벤트 매개변수를 사용하여 필터를 만듭니다: ```javascript // 특정 사용자의 언본딩만 감시 const userAddress = '0xabcd...'; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { // 지정된 사용자의 언본딩에 대해서만 실행됨 showNotification(`${ethers.formatEther(amount)} 토큰의 언본딩이 완료되었습니다!`); refreshUserBalance(userAddress); }); ``` 검증자별 대시보드를 구축하는 경우 검증자별로 필터링할 수도 있습니다: ```javascript // 특정 검증자의 모든 언본딩 감시 const validatorAddress = '0x1234...'; const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### 과거 이벤트 쿼리 dApp에서 과거 언본딩 완료 기록을 표시해야 하는 경우 블록 범위가 있는 이벤트 필터를 사용하여 과거 이벤트를 쿼리할 수 있습니다: ```javascript // 최근 1000개 블록에서 사용자의 모든 언본딩 가져오기 const currentBlock = await provider.getBlockNumber(); const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter( filter, currentBlock - 1000, currentBlock ); const unbondingHistory = events.map(event => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash })); console.log('최근 언본딩:', unbondingHistory); ``` ### 통합 가이드 #### 단계 1: Stable System 컨트랙트 인터페이스 추가 먼저 StableSystem 프리컴파일 인터페이스를 프로젝트에 추가합니다. Foundry 또는 Hardhat을 사용하는 경우 새 인터페이스 파일을 생성합니다: ```solidity interface IStableSystem { event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); } ``` Solidity 컨트랙트 없이 순수 프론트엔드 dApp을 구축하는 경우 이벤트에 대한 ABI 조각만 필요합니다: ```javascript const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; ``` #### 단계 2: 이벤트 리스너 설정 ethers.js provider를 초기화하고 StableSystem 프리컴파일 주소를 가리키는 컨트랙트 인스턴스를 생성합니다. 프리컴파일은 Stable 테스트넷과 메인넷 모두에서 항상 `0x00000000000....0000009999`에 배포됩니다. *참고: 프리컴파일은 아직 Stable 메인넷에 배포되지 않았으며 v1.2.0 업그레이드 후에 제공됩니다.* ```javascript const provider = new ethers.JsonRpcProvider(RPC_URL); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); ``` #### 단계 3: 애플리케이션 로직에서 이벤트 처리 이벤트를 구독하고 애플리케이션 상태를 그에 따라 업데이트합니다. 일반적인 패턴은 다음과 같습니다: * **잔액 업데이트**: 언본딩이 완료되면 사용자의 토큰 잔액을 새로 고침 * **알림 시스템**: 사용자의 언본딩이 완료될 때 토스트 알림 표시 * **대시보드 통계**: 실시간으로 스테이킹 지표 및 차트 업데이트 * **트랜잭션 기록**: 완료된 언본딩을 사용자의 활동 피드에 추가 #### 단계 4: 연결 문제 처리 이벤트 구독은 지속적인 websocket 연결에 의존하므로 프로덕션 dApp을 위한 재연결 로직을 구현합니다: ```javascript let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function setupEventListener() { const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz'); provider.on('error', (error) => { console.error('Provider 오류:', error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); stableSystem.on('UnbondingCompleted', handleUnbonding); } ``` ### 왜 이 방법인가? #### 커스텀 인덱서와 비교 이전에는 Stable SDK가 dApp 개발자에게 SDK 이벤트를 감시하고 데이터베이스에 저장하는 커스텀 인덱서를 실행하도록 요구했습니다. 이는 운영 오버헤드를 추가하고 잠재적인 실패 지점을 도입합니다. 시스템 트랜잭션을 사용하면 별도의 인덱서 인프라가 필요하지 않습니다. 이벤트는 모든 RPC 노드가 이미 인덱싱하고 제공하는 EVM의 로그 시스템을 통해 기본적으로 사용할 수 있습니다. 모든 표준 web3 라이브러리는 추가 도구 없이 이러한 이벤트를 구독할 수 있습니다. #### SDK 엔드포인트 폴링과 비교 시스템 트랜잭션이 없으면 EVM dApp은 언본딩 기간이 완료되었는지 확인하기 위해 주기적으로 Stable SDK REST 엔드포인트를 호출해야 합니다. 이는 여러 문제를 야기합니다: * **지연 시간 증가**: 5-10초의 폴링 간격은 사용자가 업데이트를 보기 전에 그만큼 기다려야 함을 의미합니다 * **더 높은 부하**: 모든 dApp 인스턴스의 엔드포인트 폴링은 RPC 인프라의 부하를 증가시킵니다 * **복잡성**: dApp은 web3 프로바이더(EVM 상호 작용용)와 Stable SDK REST 클라이언트(SDK 쿼리용)를 모두 처리해야 합니다 * **실시간 업데이트 없음**: 폴링은 본질적으로 즉각적인 알림을 제공할 수 없습니다 시스템 트랜잭션은 dApp이 이미 EVM 상호 작용에 사용하는 동일한 websocket 연결을 통해 실시간 이벤트 알림을 제공합니다. 이는 개발자 경험을 단순화하고 인프라 비용을 줄입니다. ### 보안 보증 #### 무신뢰 이벤트 발생 시스템 트랜잭션은 검증자만 실행할 수 있는 `PrepareProposal` ABCI 단계에서 생성됩니다. 사용자가 제출한 트랜잭션은 EVM의 상태 전환 로직이 StableSystem 프리컴파일 주소로의 트랜잭션만 서명 검증을 건너뛸 수 있도록 강제하기 때문에 시스템 발신자 주소(`0x8888888888888888888888888888888888888888`)를 위조할 수 없습니다. 이것은 다음을 의미합니다: * 사용자는 언본딩 완료 이벤트를 위조할 수 없습니다 * 사용자는 자신의 트랜잭션에서 `notifyUnbondingCompletions`을 호출할 수 없습니다 * `UnbondingCompleted` 이벤트를 발생시키는 유일한 방법은 Stable SDK 스테이킹 모듈에서 실제로 언본딩을 완료하는 것입니다 #### 추가 신뢰 가정 없음 시스템 트랜잭션은 블록체인 합의에 이미 필요한 것 이상의 새로운 보안 가정을 도입하지 않습니다. 검증자가 블록을 올바르게 실행하고 있다고 신뢰한다면 시스템 트랜잭션 이벤트가 Stable SDK 상태 변경을 정확하게 반영한다고 신뢰할 수 있습니다. 이벤트 발생 프로세스는 결정론적입니다: `EndBlock`에서 동일한 SDK 이벤트가 주어지면 모든 정직한 검증자는 `PrepareProposal` 중에 동일한 시스템 트랜잭션을 생성합니다. 합의 메커니즘은 검증자가 포함할 시스템 트랜잭션에 대해 합의하도록 보장합니다. #### 블록 최종성 Stable 블록체인은 StableBFT의 합의 메커니즘을 통해 빠른 최종성을 사용합니다. 블록이 커밋되면 즉시 최종화되며 재구성될 수 없습니다. 이는 `UnbondingCompleted` 이벤트를 받으면 영구적이라고 신뢰할 수 있음을 의미합니다. 확률적 최종성 체인에서처럼 여러 확인을 기다릴 필요가 없습니다. dApp은 이벤트를 받은 즉시 사용자 잔액을 업데이트하고 알림을 표시할 수 있습니다. ### 성능 및 제한 사항 #### 배치 크기 제약 각 블록은 시스템 트랜잭션을 통해 최대 100개의 언본딩 완료를 처리합니다. 이 제한은 높은 언본딩 활동 기간 동안 무제한 블록 크기를 방지하기 위해 존재합니다. 실제로 블록당 100개의 완료는 평균 블록 시간이 0.7초라고 가정할 때 분당 약 9000개의 완료 처리량을 제공합니다. 일반적인 스테이킹 활동은 이 제한에 거의 도달하지 않습니다. 예외적인 상황에서는 완료가 완전히 처리되기 전에 여러 블록 동안 큐에 대기할 수 있습니다. #### Gas 소비 시스템 트랜잭션은 실행 중에 gas를 소비하며, 이는 블록의 gas 제한에서 고려됩니다. gas 비용은 처리되는 완료 수에 따라 선형적으로 증가합니다: * 기본 함수 호출: 약 21,000 gas * 이벤트 발생당: 약 3,000 gas * 상태 읽기: 완료당 약 2,000 gas 100개의 완료로 구성된 전체 배치는 약 521,000 gas를 소비합니다. Stable의 블록 gas 제한이 100,000,000이므로 이는 사용 가능한 블록 공간의 0.6% 미만을 나타냅니다. #### 알림 지연 블록 N 동안 언본딩 기간이 완료되면: 1. Stable 모듈의 `EndBlock`이 블록 N의 상태에서 완료를 큐에 넣습니다 2. 블록 N+1의 `PrepareProposal`이 시스템 트랜잭션을 생성합니다 3. 시스템 트랜잭션이 블록 N+1 동안 실행되어 이벤트를 발생시킵니다 이는 언본딩 완료와 EVM 이벤트 발생 사이에 1블록 지연(약 0.7초)이 있음을 의미합니다. 언본딩 기간 자체가 7일이기 때문에 이 지연은 허용 가능한 수준입니다. #### 고부하 시나리오 언본딩 완료가 블록당 100개보다 빠르게 도착하면 큐에 누적됩니다. 큐는 FIFO 순서로 처리되므로 가장 오래된 완료가 항상 먼저 알림을 받습니다. 지속적인 고부하 동안 큐가 일시적으로 증가할 수 있습니다. 그러나 급증이 가라앉으면 완료가 적은 후속 블록이 점차 큐를 배출합니다. 시스템은 이벤트를 삭제하지 않고 급증을 처리하도록 설계되었습니다. ### 향후 확장 시스템 트랜잭션 메커니즘은 모든 Stable SDK 작업을 EVM 이벤트로 전달하는 일반적인 패턴을 제공합니다. 현재 언본딩 완료에만 사용되지만 아키텍처는 추가 사용 사례를 포괄하도록 확장될 수 있습니다. #### 스테이킹 작업 언본딩 외에도 다른 스테이킹 이벤트가 EVM 알림을 발생시킬 수 있습니다: * 검증자의 수수료율 변경 * 검증자 Jail 및 Unjail #### 거버넌스 실행 거버넌스 제안이 통과되고 실행될 때 시스템 트랜잭션은 제안 ID 및 실행 결과와 함께 이벤트를 발생시킬 수 있습니다. 이를 통해 dApp은 거버넌스 모듈을 폴링하지 않고도 매개변수 변경 또는 업그레이드에 반응할 수 있습니다. #### 일반 이벤트 브리지 이 패턴은 각 모듈이 어떤 SDK 이벤트를 EVM에 미러링해야 하는지 등록하는 구성 가능한 이벤트 브리지로 일반화될 수 있습니다. 이는 모듈별 커스텀 로직 없이 모든 Stable SDK 작업에 대한 포괄적인 가시성을 제공합니다. 핵심 아키텍처 원칙은 시스템 트랜잭션이 블록 제안 중에 검증자에 의해서만 생성되는 프로토콜 수준 기능으로 유지된다는 것입니다. ## 기술 개요 상태 데이터베이스, 실행, 합의부터 USDT 전용 최적화에 이르기까지, Stable은 성능, 확장성, 신뢰성을 중점으로 설계되었습니다. Stable 스택의 각 구성 요소는 높은 처리량을 요구하는 워크로드 및 네트워크 전반에서 USDT 중심의 원활한 운영을 위해 최적화되어 있습니다. Tech Overview ### StableBFT 초기 Stable 블록체인은 CometBFT 기반의 맞춤형 PoS 합의 프로토콜인 **StableBFT**를 활용하여, 네트워크 전반에 걸친 높은 처리량, 낮은 지연 시간, 강력한 신뢰성을 보장합니다. 합의 성능을 더욱 최적화하기 위해, Stable은 데이터 전파와 합의 과정을 분리하고, 트랜잭션을 블록 프로포저에게 직접 브로드캐스트하는 방식을 도입할 계획입니다. 합의를 획기적으로 가속화하기 위해 Stable은 DAG 기반 **Autobahn**으로의 프로토콜 업그레이드를 계획하고 있습니다. Autobahn을 기반으로 구축된 StableBFT는 다음을 가능케 합니다: * 단일 리더 제한 제거를 통한 프로포절 병렬 처리 * 데이터 전파와 트랜잭션 순서 정렬을 분리하여 더 빠른 완결성 달성 * 강력한 BFT 메커니즘을 통한 네트워크 장애에 대한 높은 복원력 ### Stable EVM **Stable EVM**은 Stable의 이더리움 호환 실행 계층으로, 기존 이더리움 툴 및 지갑을 사용해 체인과 원활히 상호작용할 수 있도록 합니다. Stable EVM은 StableSDK와의 연결을 위해 일련의 프리컴파일을 도입하여, EVM 스마트 컨트랙트가 핵심 체인 로직에 안전하고 아토믹하게 접근할 수 있도록 지원합니다. Stable은 EVM 실행 성능을 극대화하기 위해 StableVM++를 도입할 예정이며, 이는 EVMONE과 같은 대체 EVM 구현체와 Block-STM 기반의 낙관적 병렬 실행 엔진을 통합합니다. ### StableDB **상태: v1.4.0 업그레이드에서 출시** Stable은 각 블록 생성 후 디스크 저장이 느리다는 주요 병목 현상을 해결함으로써 블록체인 속도를 개선합니다. Stable은 상태 커밋과 상태 저장을 분리하여, 블록 처리를 지연 없이 진행할 수 있도록 합니다. `mmap` 기반의 `MemDB` 및 `VersionDB`를 통해 최근 데이터는 메모리에서 처리하고, 이전 데이터는 디스크에 효율적으로 저장하여 전체 처리량을 높일 수 있습니다. ### 고성능 RPC 아무리 블록체인이 빠르더라도, RPC 레이어가 느리다면 사용자 경험이 망가질 수 있습니다. Stable은 기존 모놀리틱 RPC 구조가 리소스 충돌 및 확장성 부족 문제를 일으킬 수 있다는 점을 인식하고, 이를 재설계했습니다. Stable은 기능별로 작업 경로를 분리한 split-path 아키텍처를 도입하며, 가벼우면서도 특화된 RPC 노드를 통해 응답 시간을 크게 단축합니다. 향후 EVM 내 view 호출에 최적화된 RPC 노드 및 네이티브 인덱서를 통합하여, dApp의 온체인 데이터 접근 속도를 더욱 향상시킬 예정입니다. ## 기술 로드맵 ### 모든 레이어를 최적화하기 위한 Stable의 접근법 사용자가 트랜잭션을 제출하기부터 결과를 받기까지의 라이프사이클은 여러 단계로 구성됩니다. 우선 트랜잭션은 RPC를 통해 전파되고, 멤풀에 저장되며, 블록에 포함된 다음, 합의를 통해 검증되고 실행되어, 마침내 데이터베이스에 결과 상태가 저장됩니다. 이러한 단계를 거쳐야만 사용자는 최종 결과를 받아볼 수 있습니다. 이 중 어느 단계라도 최적화되지 않는다면, 전체 시스템의 성능이 악화됩니다. Stable은 트랜잭션 파이프라인의 각 단계를 최적화하여 성능을 극대화하고 레이턴시를 최소화하는 것을 목표로 합니다. Stable의 핵심 기술은 여러 페이즈에 걸쳐 출시될 것이며, 각각은 트랜잭션 완결성을 희생하지 않으면서 전반적인 초당 트랜잭션 수(TPS)를 증가시키도록 설계되었습니다. 아래 섹션은 현재 블록체인 아키텍처 내 일반적인 병목과, Stable이 최적화하려고 하는 것들에 대해 설명합니다. Technical Roadmap ### 페이즈 1 – USDT를 위한 기반 레이어 #### StableBFT 초기 Stable 블록체인은 StableBFT를 활용합니다. 이는 CometBFT를 기반으로 높은 처리량, 낮은 레이턴시, 그리고 강력한 신뢰성을 제공하기 위한 맞춤형 PoS 프로토콜입니다. 이는 결정론적인 완결성과 최대 1/3의 밸리데이터 장애 허용(fault tolerance)이라는 특징을 가지고 있습니다. 향후 Stable은 DAG 기반 합의로의 업그레이드를 통해 5배 빠른 합의 속도를 달성할 예정입니다. #### USDT를 네이티브 가스로 Stable에서는 USDT0를 네이티브 가스 토큰으로 사용합니다. USDT0는 가스 결제와 가치 전송을 위한 네이티브 자산으로 동시에 기능하며, `approve`, `transfer`, `transferFrom`, `permit`을 지원하는 ERC20 토큰으로도 동작합니다. #### Stable Pay & Stable Name Stable Pay은 탈중앙화 금융의 사용성을 크게 향상하기 위해 설계되었습니다. 현재 Web3 지갑들에는 가파른 학습 곡선 문제가 존재하며, Stable은 Web2.5 UX를 가진 지갑 경험을 도입하는 방식으로 이 문제를 해결합니다. 이를 통해 새로운 사용자들의 온보딩을 간소화하는 동시에, 기존 크립토 사용자들과도 호환될 수 있게 할 수 있습니다. 새로운 사용자들은 직관적인 디자인과 원활한 셋업 프로세스(소셜 로그인 등)를 통해 쉽게 온보딩할 수 있으며, 기존 크립토 유저들은 가지고 있던 지갑을 Stable에 그대로 가져와 마이그레이션 없이 사용할 수 있습니다. Stable Pay은 웹 앱과 모바일 앱 양쪽으로 제공되어, 모든 기기에서 안전하게 디지털 자산에 접근할 수 있습니다. 지갑에 더해, Stable은 복잡하고 오류가 잦은 EVM 공개 주소 포맷을 고유하고 사람이 읽을 수 있는 형태로 바꾸는 Stable Name을 도입합니다. 사용자들은 긴 16진수 문자열을 관리할 필요 없이 Stable Name으로 간편하게 토큰을 주고받을 수 있습니다. 이 방식은 거래 상 오류를 크게 줄이고 크립토 자산과 상호작용할 때의 전반적인 경험을 증진하여, Stable을 블록체인 생태계로 진입하는 강력하고 사용자 중심적인 출발점으로 만들어줍니다. ### 페이즈 2 – USDT를 위한 경험 레이어 #### 낙관적 병렬 실행 실제 운영 환경의 통계에 따르면, 전체 트랜잭션의 약 60\~80%는 서로 겹치지 않는 상태를 다루기 때문에, 병렬로 안전하게 실행될 수 있습니다. 그러나 대부분의 블록체인 시스템은 여전히 트랜잭션을 순차적으로 처리하며, 이로 인해 불필요한 지연이 발생하고 있습니다. Stable은 이러한 한계를 극복하기 위해 낙관적 병렬 실행(Optimistic Parallel Execution) 모델을 채택합니다. 초기에는 상태 충돌이 없다는 가정 하에 트랜잭션을 병렬로 실행하고, 충돌이 감지되면 해당 트랜잭션만 롤백 후 순차적으로 재실행합니다. 이 방식은 정확성을 유지하면서도 처리량을 크게 향상시킬 수 있습니다. #### State DB 최적화 블록체인 성능의 주요 병목 중 하나는 느린 디스크 I/O입니다. 블록 실행 후 변경된 상태는 디스크에 기록되어야 하며, 기존 시스템에서는 상태 저장이 완료될 때까지 다음 블록 실행이 지연된다는 문제가 존재합니다. Stable은 이를 해결하기 위해 상태 커밋과 상태 저장을 분리합니다. 밸리데이터 노드는 메모리에 최신 상태를 커밋하기만 하면 다음 블록 실행을 진행할 수 있고, 과거 상태는 디스크에 비동기적으로 저장됩니다. 이로 인해 실행에 대한 레이턴시를 줄일 수 있습니다. 또한 `mmap`이라는 메모리 매핑 파일 I/O 메커니즘을 도입하여, 파일을 메모리 배열처럼 처리하는 방식으로 스토리지 성능을 높일 수 있습니다. 즉 실시간 상태 커밋은 메모리에서, 아카이브 상태는 디스크에 저장함으로써, Stable은 디스크 I/O 지연을 최소화하고 읽기/쓰기 처리량을 높입니다. #### USDT 전송 집계 많은 양의 USDT0 전송을 한 번에 처리하기 위해, Stable은 집계 메커니즘을 구현할 예정입니다. USDT0 전송 트랜잭션들을 그룹화하여 한 번에 처리함으로써, 트랜잭션 당 오버헤드를 줄이고 전반적인 처리량을 개선할 수 있습니다. #### 보장된 블록스페이스 블록체인 인프라를 사용하는 기업들은 예측 가능한 트랜잭션 레이턴시가 필요합니다. 하지만 네트워크 혼잡 시에는 이 예측 가능성이 무너질 수 있습니다. Stable은 이를 해결하기 위해 다음과 같은 방식으로 고정된 블록스페이스를 기업에 보장합니다: * 밸리데이터 단의 커스터마이징: 밸리데이터 노드가 기업을 위해 블록스페이스 일부를 할당합니다. * 전용 RPC 노드: 보장된 트랜잭션은 별도의 멤풀과 API 엔드포인트를 통해 우선적으로 처리됩니다. 이 모델은 혼잡하거나 적대적인 네트워크 환경에서도 기업의 핵심 운영에 필요한 성능을 안정적으로 제공합니다. ### 페이즈 3 – USDT를 위한 풀스택 최적화 레이어 #### Autobahn 기반의 StableBFT를 활용한 발전된 합의 알고리즘 1세대 DAG 기반 BFT 엔진(Narwhal, Tusk)은 데이터 전파와 합의를 분리함으로써 단일 제안자가 가지던 병목을 제거합니다. 그러나 이러한 시스템을 기존의 CometBFT 환경에 직접 적용하면, 높이(height) 기반 블록 처리나 전통적인 멤풀 구조와 충돌할 수 있습니다. Autobahn은 Stable의 합의 레이어와 더 자연스럽게 통합되는 ‘DAG 기반 PBFT’ 알고리즘을 제공합니다. Autobahn 기반의 StableBFT는 다음과 같은 장점을 가집니다. * 단일 리더 제한 제거를 통한 프로포절 병렬 처리 * 데이터 전파와 트랜잭션 순서 합의의 분리를 통한 더 빠른 완결성 * 네트워크 장애에 강한 견고한 BFT 구조 이 발전된 합의 디자인은 내부 테스트의 통제된 환경 내에서 (합의 레이어 한정) 200,000 TPS를 달성하는 등 매우 높은 처리량을 지원합니다. #### StableVM++ StableVM++는 기존 Go 기반 EVM을 대체하는 고성능 C++ 실행 엔진입니다. 이는 최대 6배 빠른 실행 속도를 제공하여, EVM 트랜잭션 처리 성능을 획기적으로 향상시킬 것으로 기대됩니다. #### 고성능 RPC 고성능 탈중앙화 애플리케이션은 빠르고 정확한 RPC와 인덱싱 서비스에 의존합니다. Stable은 이를 위해 다음을 포함한 고성능 RPC 스택을 개발합니다: * 노드 단 성능 향상: 즉각적인 RPC 응답을 위한 실시간 체인 상태 처리 * 노드 통합형 인덱서: 지연 없는 API 제공을 위한 실시간 인덱싱 * 확장 가능한 Pub/Sub 구조: 이벤트 구독 및 전달을 위한 견고한 웹소켓 아키텍처 * 하이브리드 로드 밸런서: 요청 유형별 트래픽 분산으로 리소스 최적화 및 병목 최소화 이러한 최적화를 통해 Stable은 dApp 및 기업 사용자에게 안정적이고 확장 가능한 엔드포인트를 제공합니다. ## 다가오는 사용 사례 Stable은 단순 전송과 API 과금을 넘어서는 결제 패턴을 향해 구축하고 있습니다. 아래 사례들은 시간이 보장된 정산, 프라이버시를 보존하는 결제, 자율 에이전트 상거래를 다룹니다. 일부는 초기 형태로 오늘날에도 작동하며, 다른 일부는 현재 개발 중인 Stable 기능에 의존합니다. ### 보장된 정산 예약된 블록 용량으로 뒷받침되는 신뢰할 수 있는 결제 정산으로, 네트워크 상태와 관계없이 트랜잭션 포함을 보장합니다. #### 개념 일부 결제는 독립적인 전송이 아니라 시간이 정해진 정산 주기의 일부입니다. 이러한 흐름에서는 다음 상태 전환이 예정대로 진행될 수 있도록 주기가 마감되기 전에 정산이 완료되어야 합니다. 필요한 결제가 그 기간을 넘겨 지연되면 주기가 실패하거나, 다음 기간으로 넘어가거나, 수동 복구가 필요할 수 있습니다. Stable의 [보장된 블록스페이스](/ko/explanation/guaranteed-blockspace)는 적격 결제 흐름을 위해 실행 용량을 예약함으로써 이를 해결합니다. 목표는 단순히 더 빠른 정산이 아니라 정확한 타이밍 제약 내에서 운영상 신뢰할 수 있는 완료입니다. #### 예상 시나리오 토큰화된 자산 플랫폼이 몇 분마다 예정된 DvP(Delivery versus Payment) 정산을 실행합니다. 현금 부분은 배치로 제출되며, 현재 정산 주기가 마감되기 전에 전체 결제 배치가 포함된 경우에만 증권이 출고됩니다. 정상적인 조건에서는 이것이 즉시 청산됩니다. 버스트 트래픽 동안에는 부분 포함이 실패하거나 넘어간 정산 주기를 만들어냅니다. 보장된 정산을 통해 플랫폼은 결제 배치를 위한 용량을 예약하여 주기가 결정론적으로 마감될 수 있도록 합니다. #### 무엇이 이를 가능하게 하는가 보장된 블록스페이스는 온체인 결제를 일정을 잡을 수 있는 작업으로 전환합니다. 정산 주기는 엄격한 타이밍 가정으로 설계될 수 있고, 배치 결제는 단일 기간 내에서 원자적으로 커밋될 수 있으며, 상위 시스템은 블록 포함을 희망이 아닌 의존성으로 다룰 수 있습니다. ### 기밀 결제 선택된 트랜잭션 세부 정보가 공개 관찰자로부터 보호되는 동시에 거래 당사자와 권한이 있는 감사자가 검증할 수 있는 프라이버시 보존 USDT0 전송입니다. #### 개념 표준 온체인 전송은 완전히 투명합니다. 누구나 발신자, 수신자, 금액을 볼 수 있습니다. 비즈니스 결제의 경우 이러한 투명성은 체인을 모니터링하는 누구에게나 상업적으로 민감한 정보를 노출할 수 있습니다. Stable은 영지식 암호화를 사용하는 프라이버시 계층인 [기밀 전송](/ko/explanation/confidential-transfer)을 개발하고 있으며, 이는 온체인 트랜잭션에 선택적 기밀성을 가능하게 합니다. 보호된 값은 관련 당사자와 권한이 있는 규제 감사자만 접근할 수 있습니다. #### 예상 시나리오 대형 소매업체가 여러 공급업체와 재고 조달을 온체인으로 정산합니다. 투명한 체인에서는 경쟁사가 이러한 트랜잭션을 모니터링하여 공급업체 관계, 주문량, 도매 가격을 역설계할 수 있습니다. 기밀 전송을 통해 상업적으로 민감한 세부 정보가 보호되는 동시에 온체인 기록은 여전히 양 당사자와 권한이 있는 감사자를 위한 검증 가능한 정산 영수증 역할을 합니다. ### 에이전트 간 결제 트랜잭션 루프에서 사람의 승인이나 개입 없이 AI 에이전트 간에 자율적으로 시작되고 정산되는 결제입니다. #### 개념 AI 에이전트가 더 많은 운영 작업을 맡게 되면서, 다른 에이전트로부터 서비스를 조달해야 할 것입니다. 현재 워크플로에서는 각 구매를 승인하거나, 공급업체를 선택하거나, 상대방이 신뢰할 수 있는지 검증하기 위해 사람이 루프에 있어야 합니다. 에이전트 간 결제는 에이전트가 단일 트랜잭션 루프 내에서 자율적으로 서비스를 찾고, 평가하고, 결제할 수 있도록 하여 이러한 병목 현상을 제거합니다. 이 패턴은 함께 작동하는 여러 신흥 프로토콜에 의존합니다: 에이전트 발견 및 신뢰([ERC-8004](https://eips.ethereum.org/EIPS/eip-8004)), 보안 통신([XMTP](https://xmtp.org)), 그리고 실시간으로 정산할 수 있는 결제 레일([x402](/ko/explanation/x402)). #### 예상 흐름 1. **발견**: 구매자 에이전트가 ERC-8004 신원 레지스트리에 쿼리하여 필요한 기능(예: 이미지 생성)을 제공하는 에이전트를 찾습니다. 레지스트리는 관련 메타데이터와 함께 일치하는 에이전트 신원을 반환합니다. 2. **검증**: 구매자가 각 후보에 대해 ERC-8004 레지스트리를 확인합니다. 신원, 평판 점수, 검증 증명이 어떤 제공자가 거래하기에 충분히 신뢰할 수 있는지 결정합니다. 3. **협상**: 구매자가 XMTP를 통해 선택된 제공자에게 작업 매개변수를 보냅니다. 두 에이전트는 암호화된 메시징을 통해 가격, 마감일, 산출물 형식에 합의합니다. 4. **결제**: 구매자가 제공자의 HTTP 엔드포인트를 호출합니다. 제공자가 402로 응답합니다. 구매자가 ERC-3009 권한을 서명하고 결제 헤더와 함께 재시도합니다. 퍼실리테이터가 Stable에서 결제를 정산하고, 제공자가 결과를 반환합니다. 5. **평가**: 전달 후, 구매자가 ERC-8004 평판 레지스트리에 피드백을 게시하여 향후 상호작용을 위한 제공자의 점수를 업데이트합니다. #### 무엇이 이를 가능하게 하는가 에이전트 간 결제는 서비스 조달을 완전히 프로그래밍 가능한 루프로 전환합니다. 에이전트는 사람의 일정 조정이나 승인 대기열 없이 실시간으로 제공자를 비교하고, 공급업체를 전환하고, 결제를 정산할 수 있습니다. 이를 통해 에이전트가 지속적으로 서비스를 조달하고, 결제하고, 제공하는 자율 공급망을 머신 속도로 구축할 수 있게 되며, 수동 조정으로 지원할 수 있는 수준을 넘어 상거래를 확장합니다. ### 다음 추천 * [**보장된 블록스페이스**](/ko/explanation/guaranteed-blockspace) — 보장된 정산 뒤에 있는 프로토콜 수준의 메커니즘을 검토하세요. * [**기밀 전송**](/ko/explanation/confidential-transfer) — Stable이 구축하고 있는 프라이버시 모델을 살펴보세요. * [**x402**](/ko/explanation/x402) — 에이전트 간 흐름 뒤에 있는 정산 프로토콜을 이해하세요. ## USDT as Gas Stable은 USDT 스테이블코인을 중심으로 구축되었습니다. USDT의 옴니체인 표현인 USDT0은 Stable 생태계를 지원하는 핵심 자산입니다. ### 요약 Stable은 USDT0을 네이티브 가스 토큰으로 사용하는 EVM 호환 블록체인입니다. USDT0은 가스 지불 및 가치 전송을 위한 네이티브 자산이면서 동시에 `approve`, `transfer`, `transferFrom` 및 `permit`를 지원하는 ERC20 토큰으로 기능합니다. 이 설계는 예측 가능한 달러 표시 트랜잭션 비용을 가능하게 하고 사용자 경험을 단순화합니다. 그러나 이는 잔액 의미론, 허용 안전성 및 특정 opcode 가정에 영향을 미치는 이더리움과의 행동 차이를 도입합니다. 본 문서는 Stable의 USDT0 가스 메커니즘을 명시하고, 결과적인 행동 차이를 설명하며, Stable에 배포된 스마트 컨트랙트에 필요하고 권장되는 개발 패턴을 정의합니다. ### 버전 참고사항 Stable v1.2.0에서 USDT0이 gUSDT를 대체하여 Stable의 네이티브 가스 토큰이 됩니다. 이 전환의 일부로: * gUSDT가 폐지됩니다. * 기존 gUSDT 잔액이 자동으로 USDT0으로 전환됩니다. * 사용자와 애플리케이션은 더 이상 수수료를 지불하거나 가치를 이동하기 위해 래핑 및 언래핑 플로우가 필요하지 않습니다. v1.2.0 이후 USDT0은 다음 두 가지 역할을 합니다: * 네트워크 수수료 자산(가스), 그리고 * `approve`, `permit`, `transfer` 및 `transferFrom`을 갖춘 표준 ERC20 토큰. ### 네트워크 주소 USDT0 토큰 컨트랙트 주소: * 테스트넷: [0x78cf24370174180738c5b8e352b6d14c83a6c9a9](https://testnet.stablescan.xyz/token/0x78cf24370174180738c5b8e352b6d14c83a6c9a9) * 메인넷: [0x779ded0c9e1022225f8e0630b35a9b54be713736](https://stablescan.xyz/token/0x779ded0c9e1022225f8e0630b35a9b54be713736) ### 용어 * **Stable**: USDT0이 네이티브 가스 토큰인 EVM 호환 블록체인. * **USDT0**: USDT의 옴니체인 버전으로 다음 두 가지로 기능합니다: * 가스 및 가치 전송에 사용되는 네이티브 자산, 그리고 * approve 및 permit 기능을 가진 ERC20 토큰. * **네이티브 잔액**: `address(x).balance`로 반환되는 USDT0으로 표시된 잔액. * **가스 수수료**: EIP-1559 스타일 수수료 시장에서 계산된 USDT0으로 지불하는 트랜잭션 수수료. ### USDT0이란 무엇인가? USDT0은 LayerZero의 옴니체인 대체 가능 토큰 (OFT) 표준을 사용하는 USDT의 옴니체인 표현입니다. USDT0은 USDT와 1:1로 페깅되어 있으며 전통적인 브리지 워크플로우나 래핑된 표현 없이 여러 블록체인을 이동하도록 설계되었습니다. 체인 간에 USDT0을 전송할 때, 토큰은 일부 소스 체인에서 잠기거나(체인의 네이티브 USDT 지원에 따라) 소각된 후 LayerZero의 크로스체인 메시징을 통해 목적지 체인에서 발행됩니다. 이는 1:1 페그를 유지하면서 유동성을 분산된 체인 로컬 풀이 아닌 단일 상호 운용 가능한 자산으로 통합합니다. 사용자에게 이는 더 빠른 온보딩, 운영 복잡성 감소 및 향상된 유동성 이동성을 가능하게 합니다. ### USDT0과 Stable USDT0은 Stable의 온체인 경제와 일상적인 사용을 지원하는 핵심 자산입니다. 동일한 자산이 수수료 지불과 가치 전송 모두에 사용되기 때문에 Stable은 다음에 대한 마찰을 줄입니다: * **일반 사용자**: 더 간단한 온보딩과 더 적은 토큰 개념 * **개발자**: 더 간단한 수수료 및 가치 흐름 * **기업**: 단순화된 회계 및 재무 운영 Stable은 또한 사용자가 LayerZero를 통해 다른 네트워크에서 USDT0을 온보딩할 수 있도록 하여 첫날부터 깊은 USDT 유동성에 액세스할 수 있습니다. ### 가정 및 전제 조건 아래 내용의 경우 독자는 다음을 이해해야 합니다: * Solidity 실행 의미론 및 네이티브 가치 전송 * ERC20 허용 메커니즘 및 허가 플로우 * Checks-Effects-Interactions를 포함한 표준 스마트 컨트랙트 보안 패턴 ### 1. 가스 및 수수료 모델 #### 1.1 개요 Stable은 모든 트랜잭션 수수료를 USDT0으로 표시합니다. 가스 가격 책정은 동적으로 조정되는 기본 수수료가 있는 EIP-1559 스타일 모델을 따릅니다. 트랜잭션 수수료는 다음과 같이 정의됩니다: ``` fee = gasUsed × baseFee ``` 트랜잭션은 표준 EIP-1559 매개변수를 사용하여 `maxFeePerGas`를 지정할 수 있습니다. *참고: Stable은 우선순위 팁을 지원하지 않습니다. `maxPriorityFeePerGas`를 설정하지 마세요. 그렇지 않으면 팁 금액이 손실됩니다.* #### 1.2 트랜잭션 제출 클라이언트는 가장 최근 블록에서 최신 기본 수수료를 가져오고 `maxFeePerGas`를 계산할 때 안전 마진을 포함해야 합니다. 예시(설명용): ```javascript const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const maxPriorityFeePerGas = 1n; const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; ``` #### 1.3 USDT0 획득 계정은 다음을 통해 USDT0을 얻습니다: * 다른 지원되는 체인에서 USDT0 브리징 * Stable의 다른 계정으로부터 전송 받기 ### 2. Stable이 USDT0을 가스 토큰으로 활성화하는 방법 Stable은 사전 청구 및 환불 결산 모델을 사용하여 USDT0으로 가스를 청구합니다. #### 예시 트랜잭션 Alice가 Bob에게 100 USDT0을 보냅니다. #### 2.1 Ante-handler 단계 `MonoEVMAnteHandler`에서 트랜잭션 검증 중: 1. Alice의 USDT0 잔액을 읽습니다. 2. 프로토콜은 Alice가 다음을 커버할 수 있는지 확인합니다: * 트랜잭션 가치(100 USDT0), 그리고 * 최대 가능 가스 수수료(`gasWanted × fee`). 3. 최대 가스 수수료가 사전에 이체됩니다: * `alice → fee_collector`로 USDT0. #### 2.2 실행 단계 `ApplyTransaction` 동안: 1. EVM이 트랜잭션을 실행합니다. 2. 실제 가스 소비가 기록됩니다. 3. 가치 전송이 적용됩니다: * `alice → bob`이 100 USDT0을 전송합니다. #### 2.3 결산 단계 실행 후: 1. 프로토콜은 사전 청구된 수수료의 사용되지 않은 부분을 계산합니다: ``` refund = (gasWanted − gasUsed) × baseFee ``` 2. 사용되지 않은 수수료가 환불됩니다: * `fee_collector → alice`로 USDT0. ### 3. 잔액 의미론 및 행동 차이 #### 3.1 네이티브 잔액 가변성 이더리움에서는 컨트랙트의 네이티브 잔액이 일반적으로 컨트랙트 실행의 결과로만 변경됩니다. Stable에서는 컨트랙트의 네이티브 USDT0 잔액이 `transferFrom` 및 `permit`를 포함한 ERC20 허용 기반 작업으로 인해 변경될 수도 있습니다. 이러한 작업은 컨트랙트 코드를 호출하지 않고도 컨트랙트의 네이티브 잔액을 줄일 수 있습니다. 결과적으로 다음 가정은 Stable에서 유효하지 않습니다: * 컨트랙트의 네이티브 잔액은 컨트랙트가 호출된 경우에만 감소할 수 있습니다. ### 4. 컨트랙트 설계 요구 사항 #### 4.1 금지된 패턴: 미러 잔액 회계 컨트랙트는 네이티브 잔액을 미러링하기 위해 내부 변수에 의존해서는 안 됩니다. 안전하지 않은 패턴의 예: ```solidity uint256 public deposited; function deposit() external payable { deposited += msg.value; } ``` 허용 기반 전송을 통해 USDT0이 고갈되면 이러한 변수는 실제 네이티브 잔액과 달라질 수 있습니다. #### 4.2 필수 패턴: 실제 잔액 지불 능력 확인 모든 네이티브 가치 전송은 전송 직전에 `address(this).balance`를 사용하여 지불 능력을 확인해야 합니다. 예시: ```solidity require(address(this).balance >= amount, "insufficient balance"); ``` 출금은 Checks-Effects-Interactions 순서를 따라야 합니다: ```solidity uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount); payable(msg.sender).call{value: amount}(""); ``` #### 4.3 상태 진행은 잔액과 독립적이어야 함 진행, 마일스톤 또는 완료 조건에 의존하는 프로토콜 로직은 카운터나 에포크와 같은 비잔액 상태 변수를 사용하여 명시적으로 추적해야 합니다. 네이티브 잔액은 지불 시점의 지불 능력 확인에만 사용되어야 합니다. #### 4.4 허용 노출 사용자 자금을 보관하는 컨트랙트는 외부 주소에 USDT0 허용을 부여해서는 안 됩니다. 허용이 불가피한 경우 컨트랙트는: * 정확한 금액만 승인 * 사용 후 즉시 허용 재설정 * 잔여 고갈 위험을 알려진 제한 사항으로 처리 ### 5. 주소 상태 가정 #### 5.1 EXTCODEHASH 컨트랙트는 `EXTCODEHASH(addr) == 0x0`에 의존하여 주소가 한 번도 사용되지 않았다고 추론해서는 안 됩니다. 주소 사용에 대한 모든 개념은 컨트랙트 상태 내에서 명시적으로 추적되어야 합니다. 예시: ```solidity mapping(address => bool) public used; ``` ### 6. 제로 주소 처리 Stable에서: * `address(0)`으로의 native value 전송은 실패합니다. * `address(0)`으로의 ERC20 USDT0 전송도 실패합니다. 제로 주소로 전송하여 USDT0을 소각하는 지원되는 메커니즘은 없습니다. 컨트랙트는 다음을 해야 합니다: * `address(0)`을 수신자로 명시적으로 거부 * 제로 주소 소각을 가정하는 모든 로직을 재설계 * 되돌릴 수 없는 손실 의미론이 필요한 경우 명시적 싱크 컨트랙트 사용 ### 7. 테스트 요구 사항 Stable 배포를 위한 테스트 스위트에는 다음이 포함되어야 합니다: * 허용 기반 고갈 시나리오 (`approve` + `transferFrom`) * 실제 네이티브 잔액을 사용한 지불 능력 시행 * `EXTCODEHASH`에 의존하지 않는 주소 사용 로직 * 제로 주소 전송에 대한 명시적 실패 케이스 ### 8. 마이그레이션 체크리스트 이더리움에서 Stable로 컨트랙트를 마이그레이션 할 때: * 컨트랙트 내부에서 변수로 네이티브 잔액을 미러링하는 로직 제거 * 모든 지불 능력 확인을 `address(this).balance`로 교체 * `address(0)`으로의 모든 네이티브 또는 ERC20 전송 제거 * 모든 USDT0 `approve`에 대한 검사 * `approve` 및 `permit` 기반 플로우를 다루는 테스트 추가 ### 9. 요약 Stable의 USDT0을 가스 토큰으로 사용하면 예측 가능한 수수료와 통합 가치 회계를 제공하는 동시에 네이티브 잔액 동작에 대한 핵심 가정을 변경합니다. Stable에서의 올바른 컨트랙트 설계는 다음을 필요로 합니다: * USDT0을 이중 역할 자산으로 처리 * 실제 잔액에 대한 지불 능력 시행 * `approve` 기반 잔고 고갈 경로 회피 * Ethereum invariant에 의거한 잔고에 대한 의존성 제거 ### FAQ **현재 USDT0을 래핑된 네이티브 토큰으로 사용하고 있습니다. 업그레이드 후에는 어떤 토큰을 래핑된 네이티브로 처리해야 하나요?** 업그레이드 후 USDT0은 네이티브 토큰이자 ERC-20 토큰이 됩니다. USDT0을 직접 사용해야 하며, 래핑이나 언래핑은 더 이상 필요하지 않습니다. **원래 USDT0 컨트랙트 주소(`0x779Ded0c9e1022225f8E0630b35a9b54bE713736`)는 어떻게 되나요?** 아무것도 변경되지 않습니다. 동일한 주소가 유효하며 계속해서 USDT0을 나타냅니다. **업그레이드 후, 네이티브 토큰 주소는 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`인가요 (`0x0000000000000000000000000000000000001000` 대신)?** 예. 업그레이드 후 네이티브 토큰 식별자/주소는 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` 입니다. **`0x0000000000000000000000000000000000001000`은 어떻게 되나요? 여전히 gUSDT의 토큰 주소로 사용되며, 우리 측에서 유지해야 하나요?** 아니요. 제거할 수 있습니다. 업그레이드 후에는 사용되지 않습니다. **DEX calldata의 경우, 프로토콜이 "네이티브 토큰" 식별자로 `0x0000000000000000000000000000000000001000` 사용을 중단하고 대신 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`를 사용하게 되나요?** 맞습니다. 업그레이드 후 DEX는 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`를 네이티브 토큰 식별자로 사용해야 합니다. ## 개요 ### USDT 특화 기능 Stable은 USDT를 위해 제작된 레이어 1 블록체인입니다. 프로토콜의 모든 구성 요소는 USDT 전송에서의 마찰을 최소화하고, 세계에서 가장 많이 사용되는 스테이블코인에 특화된 고성능 환경을 제공하기 위해 설계되었습니다. 핵심 인프라와 더불어, Stable은 전반적인 사용자 경험을 향상하기 위해 USDT에 특화된 여러 가지 기능들을 제공합니다: * **USDT를 가스 토큰으로 사용**: USDT0를 네이티브 가스 토큰으로 사용합니다. USDT0는 가스 결제와 가치 전송을 위한 네이티브 자산으로 동시에 기능하며, `approve`, `transfer`, `transferFrom`, `permit`을 지원하는 ERC20 토큰으로도 동작합니다. * **보장된 블록스페이스:** 보장된 블록스페이스는 기관들에게 블록 용량의 일부를 고정적으로 보장해주기 위한 전용 블록스페이스 할당 모델입니다. 이를 통해 기관들은 트랜잭션의 레이턴시와 비용을 쉽게 예상할 수 있습니다. * **USDT 전송 집계**: USDT0 전송을 분리 및 집계하여, 공정성을 훼손하거나 다른 트랜잭션에 영향을 주지 않으면서 처리량을 극대화하는 특수 메커니즘입니다. * **기밀 전송**: Stable은 ZK 암호화 기술을 활용하여, 송신자 및 수신자의 주소는 온체인에서 그대로 보이되 금액은 숨겨지는 프라이빗 USDT 전송 기능을 도입할 계획입니다. 이로써 규제 준수 및 감사 가능성을 유지하면서 프라이버시를 확보할 수 있습니다. ## USDT 전송 집계 USDT 트랜잭션에 최적화된 블록체인으로써, Stable은 전반적인 시스템 반응성을 유지하면서도 매우 높은 수준의 토큰 전송량을 처리할 수 있도록 설계되었습니다. USDT 중심 성능과 일반 트랜잭션 다양성 간의 균형을 맞추기 위해, Stable은 USDT 전송 집계 메커니즘을 도입합니다. 이는 USDT0 전송을 고도로 병렬화되고 장애 허용적인 방식으로 번들링 및 처리하는 효율적이고 확장 가능한 솔루션입니다. ### 왜 USDT 전송 집계가 필요하나요? 대규모 USDT 사용을 지원하는 데 있어 핵심 과제는 처리량과 공정성을 동시에 최적화하는 것입니다: * 기존 ERC20 토큰 전송은 순차적으로 처리되므로, 높은 트랜잭션 부하 시 병목 현상이 발생합니다. * 단순히 USDT0의 처리량을 우선시하면, 다른 트랜잭션이 밀려나 체인 전반의 성능이 저하될 수 있습니다. USDT 전송 집계 설계는 USDT0 전송을 타 트랜잭션과 분리하고 최적화함으로써, 나머지 실행 파이프라인에 영향을 주지 않고 이 문제를 해결합니다. ### 병렬 집계 및 검증 > *여기서 설명하는 내용은 현재 전략을 바탕으로 한 미래 지향적인 계획입니다. 모든 로드맵 항목과 마찬가지로, 더 많은 인사이트를 확보하고 새로운 우선순위에 따라 업데이트될 수 있습니다.* 전송 집계 시스템의 핵심은 `MapReduce` 컴퓨팅 모델에서 영감을 받은 병렬 가능한 집계 및 검증 파이프라인입니다. 각 전송을 순차적으로 처리하는 대신, 시스템은 번들 단위로 연산을 수행하며, 계정 간 입출금을 집계한 후 잔액 업데이트를 실행합니다. #### 주요 단계 1. **계정 diff 집계** * 각 전송은 송신자와 수신자에 매핑됩니다. * 각 계정에 대해 토큰 이동의 순 변화(net movement)를 나타내는 diff 저널이 생성됩니다: * 총 차감액(보냄): 음수 값 * 총 수신액(받음): 양수 값 2. **잔고 검증** * 시스템은 전체 입력과 출력이 일치하는, 글로벌 잔액 불변성을 보장합니다. * 각 계정의 순 변화는 병렬로 독립적으로 검증되어 잔액이 충분한지 확인됩니다. * 잔액이 부족한 계정이 있어도 번들 실행을 멈추지 않고, 해당 계정을 별도로 표시합니다. 3. **병렬을 위한 MapReduce 모델** * **Map 단계**: 모든 입출금 전송을 기반으로 각 계정의 순 델타(net delta)를 계산합니다. * **Reduce 단계**: 이 델타들을 집계하여 최종 상태 업데이트를 결정합니다. ### 기술적 하이라이트 #### 병렬 계산 모델 * 프리컴파일 컨트랙트 내 병렬성을 활용하여 잔액 확인 및 diff 연산을 동시에 수행합니다. * 기존의 순차적 ERC20 처리 대비 실행 시간을 대폭 단축할 수 있습니다. #### 의존성 분석 * 중복 전송(예: 동일 계정에서 다수 전송)을 식별합니다. * 실패 가능성이 높은 전송(예: 잔액 부족 예상됨)을 사전 표시하여 연쇄적인 실패를 방지합니다. #### 모듈식 실패 핸들링 * 전송은 계정 단위로 격리되므로, 문제가 있는 계정만 영향을 받습니다. * 충돌 없는 전송은 정상적으로 실행 및 완료됩니다. #### 선택적 실패 처리 * 기존의 블록 내 전송 처리 방식은 성공 또는 실패 중 하나의 전체 처리 방식이었으나, Stable의 집계 모델은 계정별 실패 격리를 도입합니다: * 특정 계정이 `현재 잔액 + 순 변화 < 0` 인 경우, 해당 계정의 전송만 실패로 처리합니다. * 다른 계정이 포함된 전송은 정상적으로 진행됩니다. * 이 선택적 롤백 메커니즘은, 잘못된 전송이나 악의적 전송이 전체 번들의 무결성을 해치지 않도록 보장합니다. ### 프로포저 혹은 평판 기반 정렬 실행 최적화 및 상태 충돌 방지를 위해, Stable은 집계된 전송에 대해 사전 처리 순서 지정 메커니즘을 도입합니다: * **평판 기반 정렬**: 신뢰도 높은 이력 또는 검증된 신원을 가진 송신자는 우선 처리되어 실패 및 재정렬 위험이 줄어듭니다. * **프로포저 기반 정렬**: 신뢰할 수 있는 프로포저 노드가 트랜잭션을 정렬하여 충돌을 최소화하고 처리량을 극대화합니다. * **전송 번들 우선 처리**: 집계된 USDT 전송은 일반 트랜잭션보다 우선 순위로 처리되어 의존성 충돌을 줄이고 깔끔한 실행 윈도우를 확보됩니다. Stable의 USDT 전송 집계 메커니즘은 USDT0 전송의 처리량을 극대화하면서도 일반 트랜잭션 처리 성능을 저해하지 않는 타겟형 최적화입니다. 병렬 실행, 모듈식 오류 처리, 스마트 정렬 전략을 결합함으로써, Stable은 빠르고 빈번하며 마찰 없는 토큰 전송이 일상화된 스테이블코인 기반 경제를 위한 확장 가능한 기반을 제공합니다. ## Stable에서의 USDT0 동작 **Ethereum에서 컨트랙트를 이식하려는 경우, 배포하기 전에 이 페이지를 읽으세요.** Stable에서 USDT0는 네이티브 가스 토큰이자 동일한 잔액에 대한 ERC-20 토큰입니다. 그 결과 Ethereum에서 가정하던 네 가지 동작이 깨집니다. 컨트랙트로의 호출 없이 컨트랙트의 네이티브 잔액이 변경될 수 있고, `EXTCODEHASH`가 0과 빈 해시 사이를 오갈 수 있으며, 제로 주소로의 전송이 revert되고, 단일 논리적 전송이 소수 잔액 조정으로 인해 여러 개의 `Transfer` 이벤트를 발생시킬 수 있습니다. 이 페이지는 각 사례를 살펴보고 안전한 컨트랙트 패턴을 제공합니다. 한 섹션만 읽어야 한다면 [마이그레이션 체크리스트](#migration-checklist)를 읽으세요. 이는 Ethereum 컨트랙트를 여기로 이식하기 위한 요약입니다. ### 이중 역할 개요 Stable에서 USDT0는 네이티브 가스 토큰이자 ERC-20 토큰입니다. 이 이중 역할 모델은 잔액 동작, 컨트랙트 설계, 이벤트 처리에 영향을 미칩니다. 아래 섹션들은 이중 역할이 예상 동작을 바꾸는 모든 사례를 살펴봅니다. USDT0가 이렇게 동작하는 이유에 대한 배경은 [가스로서의 USDT](/ko/explanation/usdt-as-gas-token)를 참조하세요. 실제 전송을 통해 동작을 경험하려면 [첫 USDT0 전송하기](/ko/tutorial/send-usdt0)를 참조하세요. ### 잔액 조정 USDT0는 네이티브 자산으로는 18자리 소수점을, ERC-20 토큰으로는 6자리 소수점을 사용합니다. 네이티브 전송과 ERC-20 전송은 동일한 기저 잔액에서 작동하지만, 12자리의 정밀도 차이로 인해 전송이 정수 미만 정밀도를 포함할 때 시스템이 소수 금액을 조정해야 합니다. ``` before 0.000001 USDT0 (ERC-20) + 0.000000000000000000 USDT0 (internal) // address(account).balance = 0.000001000000000000 // USDT0.balanceOf(account) = 0.000001 if transfer 0.0000001 USDT0 to another account after 0.000000 USDT0 (ERC-20) + 0.000000900000000000 USDT0 (internal) // address(account).balance = 0.000000900000000000 // USDT0.balanceOf(account) = 0.000000 ``` 이로 인해 `address(account).balance`와 `USDT0.balanceOf(account)`가 최대 0.000001 USDT0까지 차이날 수 있습니다. ### 이벤트 처리 각 조정 전송은 추가적인 `Transfer` 이벤트를 발생시킵니다. 단일 논리적 USDT0 전송은 송신자와 수신자의 소수 잔액이 어떻게 영향을 받는지에 따라 최대 두 개의 추가 `Transfer` 이벤트를 생성할 수 있습니다. * **송신자 조정**: 송신자의 소수 잔액이 부족하면 0.000001 USDT0가 송신자에서 리저브 주소로 이동합니다. 이는 추가 `Transfer` 이벤트를 발생시킵니다. * **수신자 조정**: 수신자의 소수 잔액이 오버플로우되면 0.000001 USDT0가 리저브 주소에서 수신자로 이동합니다. 이는 추가 `Transfer` 이벤트를 발생시킵니다. * **양쪽 조정**: 동일한 전송에서 두 조건이 모두 발생하면 리저브가 우회됩니다. 송신자가 메인 전송의 일부로 0.000001 USDT0를 수신자에게 직접 전송합니다. 추가 이벤트는 발생하지 않습니다. 이러한 보조 이벤트는 리저브 주소 `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`를 포함합니다. `Transfer` 이벤트를 재생하여 USDT0 잔액을 추적하는 인덱서 및 오프체인 서비스는 이 주소로 들어오고 나가는 전송을 필터링하거나 고려해야 합니다. ### 컨트랙트 설계 요구사항 #### 네이티브 잔액 가변성 Ethereum에서는 컨트랙트의 네이티브 잔액이 일반적으로 컨트랙트 실행의 결과로만 변경됩니다. Stable에서는 컨트랙트의 네이티브 USDT0 잔액이 `transferFrom` 및 `permit`을 포함한 ERC-20 허용량 기반 작업으로 인해서도 변경될 수 있습니다. 이러한 작업은 어떠한 컨트랙트 코드도 호출하지 않고 컨트랙트의 네이티브 잔액을 줄일 수 있습니다. 그 결과, 다음 가정은 Stable에서 유효하지 않습니다. > 컨트랙트의 네이티브 잔액은 컨트랙트가 호출되는 경우에만 감소할 수 있다. #### 네이티브 잔액을 미러링하지 마세요 Ethereum에서는 내부 변수로 예치금을 추적하는 것이 일반적입니다. Stable에서는 ERC-20 `transferFrom`이 외부에서 네이티브 잔액을 고갈시킬 수 있기 때문에 이는 안전하지 않습니다. ```solidity // UNSAFE on Stable uint256 public deposited; function deposit() external payable { deposited += msg.value; } ``` #### 전송 전에 항상 실제 잔액을 확인하세요 모든 네이티브 가치 전송은 내부 회계 변수가 아니라 전송 직전에 `address(this).balance`를 사용하여 지급 능력을 검증해야 합니다. ```solidity // SAFE function withdraw() external { uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount, "insufficient balance"); payable(msg.sender).call{value: amount}(""); } ``` #### 상태 진행은 잔액과 독립적이어야 합니다 진행, 마일스톤 또는 완료 조건에 의존하는 프로토콜 로직은 카운터나 에포크와 같은 잔액이 아닌 상태 변수를 사용하여 이를 명시적으로 추적해야 합니다. 네이티브 잔액은 지급 시점의 지급 능력 검증에만 사용해야 합니다. #### 제로 주소 전송 금지 Stable에서는 `address(0)`로의 네이티브 및 ERC-20 전송이 모두 revert됩니다. ```solidity // REVERT on Stable payable(address(0)).call{value: amount}("") USDT0.transfer(address(0), amount); ``` 네이티브 USDT0를 전송하는 모든 컨트랙트 로직은 수신자를 검증하고 전송 호출 전에 `address(0)`을 명시적으로 거부해야 합니다. ```solidity // SAFE require(recipient != address(0), "zero address recipient"); payable(recipient).call{value: amount}(""); ``` 컨트랙트가 제로 주소 전송을 소각 메커니즘으로 사용하는 경우, 재설계해야 합니다. 되돌릴 수 없는 손실 의미론이 필요한 경우 명시적인 싱크 컨트랙트를 사용하세요. #### EXTCODEHASH 동작 Ethereum에서 `EXTCODEHASH` opcode는 다음을 반환합니다. * **제로 해시** (`0x0000...`): 주소가 한 번도 사용된 적이 없는 경우 (nonce=0, balance=0, 코드 없음). * **빈 해시** (`0xc5d2…a470`, 빈 코드의 Keccak-256 해시): 주소가 존재하지만 코드가 없는 경우. Ethereum에서는 주소가 제로 해시에서 빈 해시로 전환되면 다시 제로 해시로 돌아갈 수 없습니다. Stable에서는 USDT0가 `permit()` 기반 승인을 지원하기 때문에, 주소가 트랜잭션을 보내지 않고도 승인을 생성할 수 있습니다. 이는 `transferFrom()`과 결합하여 nonce 증가 없이 네이티브 잔액 변경을 허용하며, 잠재적으로 `EXTCODEHASH`가 제로 해시와 빈 해시 사이를 오갈 수 있게 합니다. ```solidity // UNSAFE on Stable function isUnusedAddress(address addr) public view returns (bool) { bytes32 codeHash; assembly { codeHash := extcodehash(addr) } return codeHash == bytes32(0); } ``` 대신 명시적 추적을 사용하세요. ```solidity // SAFE contract SafeAddressTracker { mapping(address => bool) public hasBeenUsed; function markAsUsed(address addr) internal { hasBeenUsed[addr] = true; } function isUnused(address addr) public view returns (bool) { return !hasBeenUsed[addr]; } } ``` ### 테스트 요구사항 Stable 배포를 위한 테스트 스위트에는 다음이 포함되어야 합니다. * 허용량 기반 고갈 시나리오 (`approve` + `transferFrom`) * 실제 네이티브 잔액을 사용한 지급 능력 강제 * `EXTCODEHASH`에 의존하지 않는 주소 사용 로직 * 제로 주소 전송에 대한 명시적 실패 사례 ### 마이그레이션 체크리스트 Ethereum에서 Stable로 컨트랙트를 이식할 때: * 내부 네이티브 잔액 미러를 제거하세요 * 모든 지급 능력 확인을 `address(this).balance`로 교체하세요 * `address(0)`로의 모든 네이티브 또는 ERC-20 전송을 제거하세요 * 모든 USDT0 승인을 감사하세요 * `permit` 및 허용량 기반 흐름을 다루는 테스트를 추가하세요 * 오프체인 인덱서가 소수 잔액 조정에서 발생하는 보조 `Transfer` 이벤트를 처리하는지 검증하세요 ### 핵심 요약 Stable에서의 올바른 컨트랙트 설계는 다음을 요구합니다. * USDT0를 이중 역할 자산으로 취급하기 * 실제 잔액에 대해 지급 능력 강제하기 * 허용량 기반 고갈 경로 피하기 * Ethereum 특유의 잔액 및 주소 가정에 대한 의존성 제거하기 오프체인 서비스 및 인덱서는 다음을 수행해야 합니다. * 소수 잔액 조정에서 발생하는 보조 `Transfer` 이벤트를 고려하기 * 이벤트 기반 잔액 재구성 대신 직접 잔액 조회 사용하기 ### 다음 추천 * [**가스로서의 USDT**](/ko/explanation/usdt-as-gas-token) — USDT0가 네이티브 자산이자 ERC-20 토큰으로 동작하는 이유를 이해하세요. * [**첫 USDT0 전송하기**](/ko/tutorial/send-usdt0) — 네이티브 및 ERC-20 경로를 통해 테스트넷에서 USDT0 전송을 제출하세요. * [**Ethereum 비교**](/ko/explanation/ethereum-comparison) — Ethereum에서 이식할 때의 모든 동작 차이를 검토하세요. ## Stable로 브리징하기 USDT는 소스 체인에서 어떤 형태를 취하는지에 따라 두 가지 브리지 경로 중 하나를 통해 Stable에 도달합니다. 두 경로 모두 사용자의 Stable 지갑으로 USDT0를 전달합니다. :::note **두 가지 경로, 하나의 결과:** * **OFT Mesh**: 소스 체인에 이미 USDT0가 있습니다. 소스에서 소각하고 Stable에서 발행합니다. 체인 간 1:1. 예시: Arbitrum, Ethereum, Optimism, Polygon, Unichain, Ink, Bera, Mantle, Hyperliquid, MegaETH(총 21개 체인). * **Legacy Mesh**: 소스 체인에 네이티브 USDT만 있습니다. 허브 역할을 하는 Arbitrum을 통해 라우팅됩니다. 전송 금액에 0.03% 수수료가 부과됩니다. 예시: Tron, TON. ::: 아래 섹션에서 각 경로를 자세히 설명합니다. ### USDT0 OFT Mesh vs Legacy Mesh Stable은 두 개의 상호 보완적인 크로스체인 전송 네트워크에 참여합니다. #### OFT Mesh USDT0를 지원하는 모든 체인은 OFT Mesh에 참여할 수 있습니다. OFT Mesh 내에서 USDT0 크로스체인 전송은 1:1 가치 비율을 유지합니다. 전송이 발생하면 소스 체인의 USDT0 토큰이 소각되고 동일한 양이 목적지 체인에서 발행됩니다. 현재 OFT Mesh 참여자는 Arbitrum, Bera, Conflux, Ethereum, Flare, Hedera, Hyperliquid, Ink, Mantle, MegaETH, Monad, Morph, MP1, Optimism, Plasma, Polygon, Rootstock, Sei, Stable, Tempo, Unichain, X Layer를 포함합니다. #### Legacy Mesh 네이티브 USDT(USDT0가 아닌)를 가진 모든 체인은 Legacy Mesh를 통해 라우팅할 수 있습니다. Legacy Mesh는 Arbitrum이 USDT0의 중앙 허브 역할을 하는 허브 앤 스포크 아키텍처를 따릅니다. 이 모델은 Arbitrum의 USDT0 유동성 풀을 활용합니다. USDT0 팀은 전송 금액에 0.03% 수수료를 부과합니다. 현재 Legacy Mesh 참여자는 Tron과 TON을 포함합니다. Ethereum과 Arbitrum은 두 메시 모두에 참여합니다. 이러한 체인의 사용자는 OFT 경로(USDT0 소각/발행) 또는 Legacy 경로(Arbitrum 허브를 통한 네이티브 USDT 잠금)를 통해 브리징할 수 있습니다. *** ### 경로 1: USDT0를 Stable로 브리징(OFT 지원 체인) 이 경로는 사용자가 이미 Arbitrum이나 Ink 같은 OFT 지원 소스 체인에서 USDT0를 보유한 경우에 적용됩니다. #### 참여자 | 이름 | 온체인? | 책임 주체 | | --------------------- | ---- | ------------------- | | User | N/A | User | | USDT0 OUpgradable | ✅ | USDT0의 스마트 컨트랙트 | | LayerZero Endpoint V2 | ✅ | LayerZero의 스마트 컨트랙트 | | MessageLib Registry | ✅ | LayerZero의 스마트 컨트랙트 | | Executor | ❌ | LayerZero Labs | | USDT0 DVN | ❌ | USDT0 | | Canary DVN | ❌ | Canary | | LayerZero DVN | ❌ | LayerZero Labs | #### 흐름 다이어그램 USDT0를 Stable로 브리징: OFT Mesh 흐름 #### 상세 단계 ##### 1. 전송 시작(온체인, 소스 체인) 사용자는 소스 체인의 **USDT0 OUpgradable** 컨트랙트에서 `lzSend` 메서드를 호출합니다. 트랜잭션에는 메시지 페이로드, 목적지 LayerZero 엔드포인트 및 컨트랙트 주소, 그리고 가스 한도와 수수료 같은 구성 매개변수가 포함됩니다. ##### 2. 패킷 생성(온체인, 소스 체인) 소스 LayerZero Endpoint는 OApp의 메시지를 패키징하고, 지정된 소스 MessageLib 컨트랙트를 사용하여 인코딩한 다음, Security Stack(DVN) 및 Executor로 전송하여 전송 트랜잭션을 완료합니다. ##### 3. 메시지 검증(오프체인, DVN) 분산형 검증자 네트워크(DVN)는 목적지 컨트랙트가 메시지를 실행하기 전에 독립적으로 검증합니다. OApp이 인증한 DVN만 검증을 수행할 수 있습니다. USDT0 브리징에서는 모든 메시지에 LayerZero Labs, Canary, USDT0라는 세 개의 DVN이 서명해야 합니다. 모든 경로의 표준 구성은 [LayerZeroScan의 USDT0 OApp](https://layerzeroscan.com/)을 참조하세요. ##### 4. 검증 가능으로 표시(온체인, Stable) 필요한 모든 DVN이 메시지를 검증하면 목적지 MessageLib 컨트랙트가 이를 검증 가능으로 표시합니다. ##### 5. 검증 커밋(오프체인, Executor) Executor는 검증된 메시지를 목적지 LayerZero Endpoint에 커밋하여 실행을 준비합니다. ##### 6. 패킷 검증(온체인, Stable) 목적지 LayerZero Endpoint는 Executor가 전달한 패킷이 DVN이 검증한 패킷과 일치하는지 확인합니다. ##### 7. 메시지 실행(오프체인, Executor) Executor는 목적지 체인에서 `lzReceive`를 호출하여 Stable의 USDT0 OUpgradable 컨트랙트에 의한 메시지 처리를 트리거합니다. ##### 8. 완료(온체인, Stable) Stable의 USDT0 OUpgradable 컨트랙트가 검증된 메시지를 처리하여 크로스체인 전송을 완료합니다. USDT0가 사용자의 주소로 발행됩니다. *** ### 경로 2: 네이티브 USDT를 Stable로 브리징(Legacy Mesh) 이 경로는 사용자가 Tron과 같은 Legacy Mesh 체인에서 네이티브 USDT를 보유한 경우에 적용됩니다. 전송은 Stable에 도달하기 전에 중간 허브인 Arbitrum을 통해 라우팅됩니다. #### 참여자 | 이름 | 온체인? | 책임 주체 | | -------------------------- | ---- | ------------------- | | User | N/A | User | | USDT Pool | ✅ | USDT0의 스마트 컨트랙트 | | USDT0 Pool | ✅ | USDT0의 스마트 컨트랙트 | | MultiHopComposer | ✅ | LayerZero의 스마트 컨트랙트 | | USDT0 OUpgradable | ✅ | USDT0의 스마트 컨트랙트 | | LayerZero Endpoint | ✅ | LayerZero의 스마트 컨트랙트 | | MessageLib Registry | ✅ | LayerZero의 스마트 컨트랙트 | | USDT0 Legacy Mesh Operator | ❌ | USDT0 | | Executor | ❌ | LayerZero Labs | | USDT0 DVN | ❌ | USDT0 | | Canary DVN | ❌ | Canary | | LayerZero DVN | ❌ | LayerZero Labs | #### 흐름 다이어그램 Tron에서 Stable로 네이티브 USDT 브리징: Legacy Mesh 흐름 #### 상세 단계 ##### 1. 전송 시작(온체인, Tron) 사용자는 브리지 트랜잭션을 시작하고 Tron의 **USDT Pool** 컨트랙트로 네이티브 USDT를 전송합니다. USDT는 풀에 잠깁니다. 그런 다음 USDT Pool 컨트랙트는 Tron의 LayerZero Endpoint 컨트랙트로 메시지를 전송합니다. ##### 2. Legacy Mesh로 메시지 전송(오프체인) LayerZero Endpoint 컨트랙트는 메시지를 **USDT0 Legacy Mesh Operator**로 전송하고, 이는 메시지를 검증합니다. ##### 3. MultiHop 전송 시작(온체인, Arbitrum) USDT0 Legacy Mesh Operator는 Arbitrum의 LayerZero **MultiHopComposer** 컨트랙트에서 `lzCompose()` 메서드를 호출합니다. 추가적인 사용자 상호작용 없이 MultiHopComposer 컨트랙트는 Arbitrum에서 Stable로의 USDT0 발행 및 소각 브리지 전송을 수행합니다. :::note MultiHopComposer 컨트랙트는 완전히 무허가형이며 불변성을 보장하기 위해 `owner()`가 없습니다. ::: ##### 4. USDT0를 Stable로 전송(온체인 및 오프체인) 나머지 단계는 [USDT0를 Stable로 브리징](#path-1--bridging-usdt0-to-stable-oft-supported-chains)(위의 1\~8단계)과 정확히 동일한 경로를 따릅니다. Arbitrum의 USDT0 OUpgradable 컨트랙트가 LayerZero를 통해 전송하고, DVN이 검증하며, USDT0가 Stable에서 발행됩니다. #### 유의할 점 * Arbitrum의 USDT0 유동성은 USDT0 팀이 관리합니다. * Legacy Mesh는 전송 금액에 0.03% 수수료가 발생합니다. * 사용자는 Arbitrum과 직접 상호작용할 필요가 없으며, MultiHop 흐름은 자동입니다. ### 다음 권장 사항 * [**자금 흐름**](/ko/explanation/flow-of-funds) — 온램프부터 정산까지 USDT의 전체 라이프사이클을 확인하세요. * [**브리지 튜토리얼**](/ko/tutorial/bridge-usdt0) — LayerZero OFT 어댑터를 사용하여 Sepolia에서 Stable 테스트넷으로 Test USDT를 브리징하세요. * [**가스로서의 USDT**](/ko/explanation/usdt-as-gas-token) — 자산이 Stable에 도달한 후 무엇을 하는지 이해하세요. ## 결제 및 송금 P2P 결제와 가맹점 정산을, 자금을 이동시키고 거래 비용을 지불하는 하나의 자산을 중심으로 구축했습니다. ### 문제점 범용 체인에서는 스테이블코인을 이동시키기 위해 사용자가 별도의 가스 토큰(ETH, SOL)을 보유해야 합니다. 이는 "1달러를 보내면 1달러를 받는다"는 사고 모델을 깨뜨리고, USDT만 보유한 지불자가 송금조차 제출할 수 없는 온보딩 단계에서 전환율을 떨어뜨립니다. ### Stable의 해결 방식 * **USDT0는 가스 토큰이자 결제 자산입니다.** 사용자는 송금하거나 수신하는 데 단 하나의 자산만 필요합니다. [가스로서의 USDT](/ko/explanation/usdt-as-gas-token)를 참조하세요. * **가스 면제(Gas waiver)를 통해 애플리케이션이 사용자를 대신해 가스를 부담할 수 있습니다.** 이를 통해 사용자가 두 번째 토큰을 다룰 필요 없이 수수료 없는 UX를 구현할 수 있습니다. [가스 면제](/ko/explanation/gas-waiver)를 참조하세요. * **단일 슬롯 완결성(single-slot finality)은 정산이 즉각적임을 의미합니다.** 송금이 블록에 포함되면 곧바로 최종 확정됩니다. [이더리움 비교](/ko/explanation/ethereum-comparison)를 참조하세요. ### 다음 추천 항목 * [**가스로서의 USDT**](/ko/explanation/usdt-as-gas-token) — 가스와 결제를 동시에 처리하며 ETH를 대체하는 자산을 이해하세요. * [**가스 면제**](/ko/explanation/gas-waiver) — 애플리케이션이 거버넌스 승인을 받은 면제 주소를 통해 사용자 가스를 부담하는 방법을 알아보세요. * [**이더리움 비교**](/ko/explanation/ethereum-comparison) — 이더리움에서 이전할 때 변경되는 사항(완결성, 가스 토큰, 우선순위 팁)을 검토하세요. ## 급여 및 대량 지급 직원, 계약자, 공급업체에게 대규모로 지급하되, 예측 가능한 처리량, 예측 가능한 비용, 민감한 금액에 대한 프라이버시를 제공합니다. ### 문제점 대량의 스테이블코인 지급은 공유 체인에서 트랜잭션당 처리량 한계에 부딪힙니다. 비용은 네트워크 혼잡도에 따라 변동하므로, 어제는 저렴하게 처리된 급여 실행이 오늘은 급등할 수 있습니다. 게다가 급여 및 공급업체 금액이 체인을 지켜보는 누구에게나 공개적으로 보이며, 이는 상업적으로 민감한 데이터를 유출합니다. ### Stable의 해결 방법 * **USDT 전송 애그리게이터는 대량 전송을 병렬화된 정산 번들로 배치 처리하므로**, 단일 실행이 트랜잭션당 오버헤드에 병목되지 않습니다. [USDT 전송 애그리게이터](/ko/explanation/usdt-transfer-aggregator)를 참조하세요. * **보장된 블록스페이스는 등록된 파트너에게 모든 블록에서 예약된 용량을 제공하므로**, 네트워크가 무엇을 하든 상관없이 포함 여부와 비용이 예측 가능하게 유지됩니다. [보장된 블록스페이스](/ko/explanation/guaranteed-blockspace)를 참조하세요. * **컨피덴셜 전송은 영지식 암호화를 사용해 금액을 가리므로**, 급여 및 공급업체 실행이 민감한 숫자를 온체인에 공개하지 않습니다. [컨피덴셜 전송](/ko/explanation/confidential-transfer)을 참조하세요. ### 다음 추천 사항 * [**USDT 전송 애그리게이터**](/ko/explanation/usdt-transfer-aggregator) — 대량 USDT0 전송이 병렬화된 정산 번들로 배치 처리되는 방법을 이해하세요. * [**보장된 블록스페이스**](/ko/explanation/guaranteed-blockspace) — 등록된 파트너가 모든 블록에서 예약된 용량을 확보하는 방법을 확인하세요. * [**컨피덴셜 전송**](/ko/explanation/confidential-transfer) — ZK 암호화가 당사자의 감사 가능성을 유지하면서 전송 금액을 가리는 방법을 검토하세요. ## 비공개 송금 금액이 상업적으로 민감하여 외부에 공개되어서는 안 되는 자금 운용, 공급업체 결제, 급여 지급 등의 작업입니다. ### 문제 모든 표준 EVM 송금은 공개적으로 관찰 가능합니다. 급여 지급이나 공급업체 정산은 누가 누구에게, 얼마를, 얼마나 자주 지급했는지와 같은 사업상 중요한 데이터를 온체인에 노출합니다. 경쟁자, 거래 상대방, 그리고 네트워크를 스크래핑하는 누구든 별도로 묻지 않고도 급여 구간, 공급업체 가격, 자금 운용 동향을 재구성할 수 있습니다. ### Stable의 해결 방법 * **기밀 송금은 영지식 암호화를 사용하여 송금 금액을 보호**하면서도 컴플라이언스를 위해 거래 당사자에 대한 감사 가능성을 유지하므로, 민감한 수치가 감사 추적을 희생하지 않고도 비공개로 유지됩니다. [기밀 송금](/ko/explanation/confidential-transfer)을 참조하세요. * **자금 흐름은 온램프부터 오프램프까지 전체 USDT 수명 주기에서 기밀 송금이 어디에 위치하는지** 보여줍니다. [자금 흐름](/ko/explanation/flow-of-funds)을 참조하세요. ### 다음 권장 사항 * [**기밀 송금**](/ko/explanation/confidential-transfer) — ZK 암호화가 거래 당사자에 대한 감사 가능성을 유지하면서 송금 금액을 보호하는 방법을 살펴보세요. * [**자금 흐름**](/ko/explanation/flow-of-funds) — 온램프에서 온체인 송금을 거쳐 오프램프 정산까지 USDT를 추적하세요. ## 스폰서 및 가스리스 경험 가스를 사용자 경험에서 완전히 제거하고 싶어하는 앱을 위한 것으로, 처음 사용하는 사용자가 별도의 자산을 먼저 확보하지 않고도 로그인하고 거래할 수 있게 합니다. ### 문제 앱을 사용하기 전에 사용자에게 가스 토큰을 확보하도록 요구하면, 소비자 대상 제품의 전환율을 떨어뜨리는 온보딩 절벽이 생깁니다. USDT만(또는 아무것도) 가지고 온 신규 사용자는 트랜잭션을 제출할 수 없으며, 가스를 사기 위해 별도의 거래소로 유도하는 지점에서 대부분의 사용자가 이탈합니다. ### Stable의 해결 방법 * **가스 면제: 거버넌스 승인을 받은 면제 주소가 사용자를 대신해 가스 가격이 0인 래퍼 트랜잭션을 제출**하므로, 앱이 가스를 처음부터 끝까지 부담하고 사용자는 무료 동작을 보게 됩니다. [가스 면제](/ko/explanation/gas-waiver)를 참조하세요. * **EIP-7702 세션 키를 통해 dApp이 범위가 지정되고 시간 제한이 있는 권한을 보유**할 수 있으므로, 사용자가 매번 서명하지 않아도 사용자를 대신해 트랜잭션을 제출할 수 있습니다. [EIP-7702](/ko/explanation/eip-7702)를 참조하세요. ### 다음 추천 * [**가스 면제**](/ko/explanation/gas-waiver) — 거버넌스 승인을 받은 면제가 가스 가격이 0인 래퍼 트랜잭션을 어떻게 제출하는지 확인하세요. * [**EIP-7702**](/ko/explanation/eip-7702) — EOA가 범위가 지정되고 시간 제한이 있는 권한을 dApp에 어떻게 위임할 수 있는지 이해하세요. ## HTTP 엔드포인트 수익화 x402는 HTTP 위에 구축된 결제 프로토콜입니다. 서버가 `402 Payment Required`와 결제 세부 정보로 응답하면, 클라이언트가 [ERC-3009](/ko/explanation/erc-3009) 인증을 서명하고, 퍼실리테이터가 이를 온체인에서 정산합니다. 전체 교환 과정은 표준 HTTP 헤더를 통해 이루어집니다. 클라이언트는 지갑만 있으면 됩니다. 회원 가입도, API 키도, 카드 등록도 필요 없습니다. 이는 클라이언트가 리소스나 서비스에 대해 서버에 지불하는 모든 시나리오에 적용됩니다. API 접근, 디지털 콘텐츠, 가맹점 결제, 또는 에이전트 간 결제 등이 그 예입니다. ### x402와 MPP x402는 원조 HTTP-402 결제 프로토콜입니다. [Machine Payments Protocol (MPP)](/ko/explanation/mpp)은 더 광범위한 IETF 트랙의 후속 프로토콜로, 결제 인텐트(세션, 구독), 멀티 레일 지원(카드, Lightning), 그리고 프로덕션 기능(본문 다이제스트 바인딩, 멱등성)을 추가합니다. MPP 클라이언트는 하위 호환성을 가지고 있어 변경 없이 x402 서버를 호출할 수 있습니다. 오늘날 Stable에서 가장 직접적인 경로는 Semantic Pay 또는 Heurist를 통한 x402입니다. 동일한 USDT0 레일에서 MPP의 와이어 포맷을 사용하려면 [Stable에서 MPP 엔드포인트 구축하기](/ko/how-to/build-mpp-endpoint)를 참조하세요. ### 어떤 문제를 해결하나요? 오늘날 인터넷에서 서비스에 비용을 지불하려면 모든 단계에서 사용자의 개입이 필요합니다. 계정에 가입하고, 로그인하고, 결제 수단을 등록해야 합니다. 이 모델은 다음과 같은 경우로 확장되지 않습니다. * 인프라 비용을 정당화하기에는 너무 작은 서비스 * 카드 네트워크 수수료를 감당하기에는 너무 저렴한 거래 * 가입 과정을 수행할 수 없는 자율 에이전트(AI, 봇, IoT 기기) x402를 사용하면 클라이언트는 지불하기 위해 지갑만 있으면 됩니다. | **측면** | **기존 청구 방식** | **x402 사용 시** | | :----------- | :------------------ | :---------------- | | 계정 필요 | 예 | 아니오 | | API 키 필요 | 예 | 아니오 | | 실현 가능한 최소 가격 | 약 $0.30 (카드 처리 하한선) | 약 $0.001 (온체인) | | 정산 시간 | 수일 (카드 네트워크) | 1초 미만 (Stable 기준) | | PCI 준수 필요 | 예 | 아니오 | ### 작동 방식 #### 세 가지 역할 **클라이언트**는 리소스를 필요로 하는 주체입니다. 웹 앱, 백엔드 서비스, CLI 도구, 또는 AI 에이전트일 수 있습니다. 클라이언트는 지갑(ERC-3009 인증을 서명할 수 있는 개인 키)만 있으면 됩니다. **서버**는 리소스를 제공하는 주체입니다. 서버는 엔드포인트에 x402 미들웨어를 부착하여 무엇이 얼마인지 정의합니다. **퍼실리테이터**는 정산 서비스입니다. 서버로부터 서명된 결제를 받아 검증하고, 온체인 트랜잭션을 제출한 뒤 결과를 반환합니다. 퍼실리테이터는 클라이언트의 자금을 절대 보유하지 않습니다. 자금 이동은 토큰 컨트랙트 내에서 클라이언트로부터 서버로 직접 이루어집니다. Stable에서는 [Semantic Pay](https://x402.semanticpay.io)가 공개 퍼실리테이터를 운영합니다. #### 결제 흐름 1. **클라이언트가 리소스를 요청합니다.** 클라이언트는 일반 HTTP 요청(GET, POST 등)을 서버로 전송합니다. 2. **서버가 402로 응답합니다.** 서버는 HTTP `402 Payment Required`와 함께 클라이언트가 필요로 하는 모든 정보를 담은 `PAYMENT-REQUIRED` 헤더를 반환합니다. 얼마를 지불해야 하는지, 어떤 토큰인지, 어떤 네트워크인지, 그리고 자금을 어디로 보내야 하는지를 포함합니다. 3. **클라이언트가 서명하고 재제출합니다.** 클라이언트는 결제 요구 사항을 읽고, 지정된 금액에 대해 ERC-3009 인증을 서명한 뒤, 서명된 인증을 담은 `PAYMENT-SIGNATURE` 헤더와 함께 원래 요청을 재제출합니다. 4. **퍼실리테이터가 검증하고 정산합니다.** 서버는 서명된 결제를 퍼실리테이터로 전달합니다. 퍼실리테이터는 서명을 검증하고, `transferWithAuthorization` 호출을 온체인에 제출하며, 확인이 완료되면 서버는 정산 영수증을 담은 `PAYMENT-RESPONSE` 헤더와 함께 요청된 리소스를 반환합니다. #### 세 가지 헤더 모든 결제 정보는 Base64로 인코딩되어 표준 HTTP 헤더를 통해 전달됩니다. | **헤더** | **방향** | **내용** | | :------------------ | :---------- | :------------------------------------ | | `PAYMENT-REQUIRED` | 서버에서 클라이언트로 | 결제 스킴, 토큰 주소, 금액, 수신자 주소, 네트워크 식별자 | | `PAYMENT-SIGNATURE` | 클라이언트에서 서버로 | 클라이언트가 이체를 승인했음을 증명하는 서명된 ERC-3009 인증 | | `PAYMENT-RESPONSE` | 서버에서 클라이언트로 | 트랜잭션 해시 및 확인 상태를 포함한 정산 결과 | 이 설계는 모든 HTTP 클라이언트, 모든 프로그래밍 언어, 그리고 커스텀 헤더를 지원하는 모든 인프라에서 작동합니다. ### Stable의 x402 x402 프로토콜은 HTTP를 통해 결제가 작동하는 방식을 정의합니다. Stable은 이를 프로덕션 환경에서 실용적으로 만드는 정산 환경을 제공합니다. #### 1초 미만의 완결성 Stable의 합의는 1초 미만의 블록 완결성(약 700밀리초)을 제공하여, x402 퍼실리테이터가 트랜잭션을 실시간으로 검증하고 정산할 수 있게 합니다. 이는 AI 에이전트나 IoT 기기가 여러 소액 결제를 빠르게 연속으로 실행할 수 있는 고빈도 자동화 상호작용에서 매우 중요합니다. #### 단일 자산 정산 Stable에서 USDT0은 네이티브 가스 토큰이자 결제 토큰입니다. 전체 x402 결제 수명 주기는 USDT0만으로 실행됩니다. 클라이언트는 USDT0만 보유하고, 퍼실리테이터는 정산에 사용하는 것과 동일한 토큰으로 트랜잭션을 제출합니다. x402를 사용하는 AI 에이전트의 경우, 이는 에이전트 지갑이 단 하나의 자산만 관리하면 된다는 것을 의미합니다. #### 마이크로 가격 책정 가격은 USDT0 원자 단위(소수점 6자리)로 표시됩니다. 비용 매개변수 `"1000"`은 정확히 $0.001로 변환됩니다. 이 정밀도는 x402 서버가 센트 단위 이하로 가격을 설정할 수 있게 합니다. #### 가스 면제 통합 [가스 면제](/ko/how-to/integrate-gas-waiver)는 트랜잭션 비용을 완전히 제거합니다. x402 퍼실리테이터는 가스 면제 인프라를 사용하여 구매자나 판매자 어느 쪽에도 가스를 청구하지 않고 `transferWithAuthorization` 호출을 제출할 수 있습니다. 이는 Stable에서의 x402 마이크로페이먼트가 결제 금액 자체 외에는 어떠한 추가 부담도 발생하지 않음을 의미합니다. ### 인프라 #### Semantic Pay [Semantic Pay](https://x402.semanticpay.io)는 Stable을 위한 공개 x402 퍼실리테이터를 제공합니다. 이는 서명 검증, 온체인 제출, 그리고 확인 추적을 처리합니다. Stable에서 x402를 통합하는 개발자는 자체 정산 인프라를 운영하지 않고도 미들웨어를 이 엔드포인트로 지정할 수 있습니다. **퍼실리테이터 엔드포인트:** `https://x402.semanticpay.io` #### WDK (Wallet Development Kit) AI 에이전트가 자율적으로 x402에 참여하려면 프로그래밍 방식으로 제어할 수 있는 지갑이 필요합니다. Tether의 오픈소스 WDK가 이를 제공합니다. * **자기 수탁(Self-custody)**: WDK는 AI 에이전트가 중앙화된 API 인프라에 의존하지 않고 로컬에서 개인 키를 생성하고 저장할 수 있게 합니다. * **x402 호환성**: WDK의 `WalletAccountEvm` 인스턴스는 x402 SDK가 요구하는 클라이언트 서명자 인터페이스를 기본적으로 충족하여, 에이전트가 402 HTTP 응답을 자동으로 가로채고, ERC-3009 인증을 서명하며, 요청을 재제출할 수 있게 합니다. **참고 자료:** * [ERC-3009 (Transfer With Authorization)](/ko/explanation/erc-3009): x402가 사용하는 온체인 정산 표준 * [결제 사용 사례](/ko/explanation/payment-use-cases-overview): P2P, 구독, 인보이스, API 청구 패턴 * [가스 면제](/ko/how-to/integrate-gas-waiver): 비용 없는 트랜잭션 제출 ## Integrate Stable Stable is a Layer 1 where USDT0 is both the native gas token and an ERC-20. Single-slot finality, sub-second block times, and full EVM compatibility. You bring your wallet, seed phrase, and USDT0. Pick the capability you're building. Every path below leads to a runnable guide on testnet within minutes. ### Pick your path * [**Accounts**](/en/explanation/accounts-overview) — Wallets, EIP-7702 delegation, session keys, and spending limits. First-class support for user and agent accounts. * [**Payments**](/en/explanation/payments-overview) — Send USDT0, build P2P wallets, recurring subscriptions, invoice settlement, and pay-per-call APIs. * [**Contracts**](/en/explanation/contracts-overview) — Deploy, verify, and index contracts. Call Bank, Distribution, and Staking precompiles from Solidity. * [**AI / Agents**](/en/explanation/agent-settlement) — Wire MCP servers and agent skills into AI editors. Price APIs per request for autonomous agents. * [**Infrastructure**](/en/explanation/integrate-overview) — Gas waiver service, ecosystem providers (bridges, oracles, ramps), network info, and node operations. * [**Learn**](/en/explanation/learn-overview) — Architecture, USDT0 behavior, use case narratives, and the Ethereum-to-Stable reference. ### Start in five minutes * [**Quick start**](/en/tutorial/quick-start) — Connect, fund a wallet from the faucet, and send 0.001 USDT0 natively. * [**Connect to Stable**](/en/reference/connect) — Chain IDs, RPC endpoints, faucet, and block explorers. * [**Difference from Ethereum**](/en/explanation/ethereum-comparison) — What stays the same and what changes when porting from Ethereum. ### Everything else * **Network status and versions**: [testnet](/en/reference/testnet-information) · [mainnet](/en/reference/mainnet-information) · [version history](/en/reference/testnet-version-history). * **Tokenomics and roadmap**: [STABLE tokenomics](/en/reference/tokenomics) · [technical roadmap](/en/explanation/technical-roadmap). * **FAQ**: [developer FAQ](/en/reference/faq) · [developer assistance](/en/reference/developer-assistance). ## Bridge USDT0 to Stable In this tutorial, you will bridge USDT0 from Ethereum Sepolia to the Stable Testnet programmatically using TypeScript and ethers v6. You will build the script incrementally, adding one function per step. This tutorial uses the OFT Mesh path. The OFT Adapter on Sepolia locks your tokens, LayerZero's dual-DVN verification confirms the message, and USDT0 is minted on Stable. For a full explanation of how this works, see [Bridging to Stable](/en/explanation/usdt0-bridging). :::note Want fewer lines of code? The [Stable SDK](/en/explanation/sdk-overview) exposes `quoteBridge` and `bridge` and picks the route (LayerZero or LI.FI) for you. ::: ### Prerequisites * Node.js 18.0.0 or higher (`node --version` to verify) * A Sepolia wallet with a private key you control (never use a key holding real funds) * SepoliaETH for gas (get some from [sepoliafaucet.com](https://sepoliafaucet.com) or [faucets.chain.link/sepolia](https://faucets.chain.link/sepolia)) * Basic familiarity with running scripts from the terminal *** ### 1. Set up the project ```bash mkdir stable-bridge && cd stable-bridge npm init -y npm install ethers@6 @layerzerolabs/lz-v2-utilities npm install -D tsx ``` Your `package.json` should include: ```json { "name": "stable-bridge", "version": "1.0.0", "scripts": { "bridge": "tsx --env-file=.env bridge.ts" }, "dependencies": { "@layerzerolabs/lz-v2-utilities": "^2.3.39", "ethers": "^6.13.0" }, "devDependencies": { "tsx": "^4.19.0" } } ``` ### 2. Configure your environment Create a `.env` file with your credentials: ```bash PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE SEPOLIA_RPC_URL=https://rpc.sepolia.org ``` For `SEPOLIA_RPC_URL`, any of these work: * Public: `https://rpc.sepolia.org` or `https://ethereum-sepolia-rpc.publicnode.com` * Alchemy: `https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY` * Infura: `https://sepolia.infura.io/v3/YOUR_KEY` ### 3. Scaffold the script Create `bridge.ts` with the imports, configuration, and a `main` function. You will add functions to this file in the following steps, and call them from `main`. ```ts import { ethers, Contract, Wallet, JsonRpcProvider } from "ethers"; import { Options } from "@layerzerolabs/lz-v2-utilities"; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org"; // Contract addresses const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927"; const SEPOLIA_OFT_ADAPTER = "0xc099cD946d5efCC35A99D64E808c1430cEf08126"; const STABLE_USDT0 = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; // Destination: Stable Testnet const STABLE_TESTNET_EID = 40374; // Minimal ABIs — only the functions we call const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function approve(address, uint256) returns (bool)", "function allowance(address, address) view returns (uint256)", "function mint(address, uint256)", ]; const OFT_ADAPTER_ABI = [ "function quoteSend((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), bool) view returns ((uint256 nativeFee, uint256 lzTokenFee))", "function send((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), (uint256 nativeFee, uint256 lzTokenFee), address) payable returns ((bytes32, uint64, (uint256, uint256)), (uint256, uint256))", ]; function addressToBytes32(addr: string): string { return ethers.zeroPadValue(ethers.getBytes(ethers.getAddress(addr)), 32); } // You will add functions here. async function main() { const provider = new JsonRpcProvider(SEPOLIA_RPC_URL); const wallet = new Wallet(PRIVATE_KEY, provider); const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet); const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet); const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals) // You will add function calls here. } main().catch((err) => { console.error(err.message); process.exit(1); }); ``` ### 4. Mint test USDT0 on Sepolia The test USDT0 contract on Sepolia exposes a public `mint` function. Add the following function to `bridge.ts` above `main`: ```ts async function mint(usdt0: Contract, receiver: string, amount: bigint) { console.log(`Minting ${ethers.formatEther(amount)} USDT0 on Sepolia...`); const tx = await usdt0.mint(receiver, amount); await tx.wait(); console.log(`Mint tx: ${tx.hash} confirmed`); const balance = await usdt0.balanceOf(receiver); console.log(`USDT0 balance: ${ethers.formatEther(balance)}`); } ``` Then call it from `main`: ```ts await mint(usdt0, wallet.address, amount); ``` Run the script: ```bash npx tsx --env-file=.env bridge.ts ``` *** **Checkpoint:** You should see a non-zero USDT0 balance logged after the mint confirms. *** ### 5. Approve the OFT Adapter Before the OFT Adapter can move your tokens, it needs an ERC-20 allowance. Add this function above `main`: ```ts async function approve(usdt0: Contract, spender: string, owner: string, amount: bigint) { console.log("Approving OFT Adapter..."); const tx = await usdt0.approve(spender, amount); await tx.wait(); console.log(`Approve tx: ${tx.hash} confirmed`); const allowance = await usdt0.allowance(owner, spender); console.log(`Allowance: ${ethers.formatEther(allowance)}`); } ``` Add the call in `main` after `mint`: ```ts // await mint(usdt0, wallet.address, amount); await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); ``` Run the script. You can comment out the `await mint(...)` call if you already have tokens from the previous run. *** **Checkpoint:** The script should log a non-zero allowance after the approval confirms. *** ### 6. Quote the fee and send the bridge transaction The `quoteSend` call returns the LayerZero messaging fee in SepoliaETH, which you pass as `msg.value` to `send`. Add this function above `main`: ```ts async function send(oftAdapter: Contract, receiver: string, amount: bigint) { const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(); const sendParams = { dstEid: STABLE_TESTNET_EID, to: addressToBytes32(receiver), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: "0x", oftCmd: "0x", }; console.log("Quoting bridge fee..."); const feeResult = await oftAdapter.quoteSend(sendParams, false); const fee = { nativeFee: feeResult.nativeFee, lzTokenFee: feeResult.lzTokenFee }; console.log(`Bridge fee: ${ethers.formatEther(fee.nativeFee)} ETH`); console.log("Sending bridge transaction..."); const tx = await oftAdapter.send(sendParams, fee, receiver, { value: fee.nativeFee, }); await tx.wait(); console.log(`Bridge tx: ${tx.hash} confirmed`); console.log(`Sepolia Etherscan: https://sepolia.etherscan.io/tx/${tx.hash}`); console.log(`LayerZero Scan: https://testnet.layerzeroscan.com/tx/${tx.hash}`); } ``` Add the call in `main` after `approve`: ```ts // await mint(usdt0, wallet.address, amount); // await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); await send(oftAdapter, wallet.address, amount); ``` ### 7. Verify arrival on Stable Testnet After sending, the script can poll the Stable Testnet RPC until the tokens arrive. Add this function above `main`: ```ts async function verify(receiver: string) { console.log("Waiting for DVN verification (~2 minutes)..."); const stableProvider = new JsonRpcProvider("https://rpc.testnet.stable.xyz"); const stableUsdt0 = new Contract(STABLE_USDT0, ["function balanceOf(address) view returns (uint256)"], stableProvider); const before: bigint = await stableUsdt0.balanceOf(receiver); for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 5000)); const current: bigint = await stableUsdt0.balanceOf(receiver); if (current > before) { console.log(`\nUSDT0 on Stable: ${ethers.formatEther(current)}`); console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`); return; } process.stdout.write("."); } console.log("\nTokens have not arrived yet. Check manually:"); console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`); } ``` Add the call in `main` after `send`: ```ts // await mint(usdt0, wallet.address, amount); // await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); // await send(oftAdapter, wallet.address, amount); await verify(wallet.address); ``` ### 8. Run the complete bridge Your `main` function should now look like this: ```ts async function main() { const provider = new JsonRpcProvider(SEPOLIA_RPC_URL); const wallet = new Wallet(PRIVATE_KEY, provider); const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet); const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet); const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals) await mint(usdt0, wallet.address, amount); await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); await send(oftAdapter, wallet.address, amount); await verify(wallet.address); } ``` Run it: ```bash npx tsx --env-file=.env bridge.ts ``` *** **Checkpoint:** You should see output like this: ``` Minting 1.0 USDT0 on Sepolia... Mint tx: 0x3a1f...c9d2 confirmed USDT0 balance: 1.0 Approving OFT Adapter... Approve tx: 0x7b2e...f401 confirmed Allowance: 1.0 Quoting bridge fee... Bridge fee: 0.000101 ETH Sending bridge transaction... Bridge tx: 0xa94f...8c11 confirmed Sepolia Etherscan: https://sepolia.etherscan.io/tx/0xa94f...8c11 LayerZero Scan: https://testnet.layerzeroscan.com/tx/0xa94f...8c11 Waiting for DVN verification (~2 minutes)... ...... USDT0 on Stable: 1.0 ``` You can also search your wallet address on the [Stable Testnet explorer](https://testnet.stablescan.xyz) to confirm the mint event. *** ### What you have built You bridged USDT0 from Ethereum Sepolia to Stable Testnet. You now know how to: * Mint test USDT0 on Sepolia using the contract's public `mint` function * Approve an OFT Adapter to spend ERC-20 tokens on your behalf * Construct LayerZero `sendParams` with 32-byte address encoding and executor options * Quote the cross-chain messaging fee with `quoteSend` before committing funds * Execute a cross-chain token transfer with `send` and confirm delivery on the destination chain * Verify on-chain state using Stable's RPC (`https://rpc.testnet.stable.xyz`, chain ID `2201`) and Stablescan ### Next recommended * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Use the bridged USDT0 with native and ERC-20 transfers. * [**Bridging to Stable**](/en/explanation/usdt0-bridging) — Deep dive on OFT Mesh vs Legacy Mesh mechanics. * [**Testnet information**](/en/reference/testnet-information) — Full network parameters, RPC endpoints, and faucet details. ## Quick start The only tools you need are Node.js, some USDT0 from the faucet and a private key. Stable uses USDT0 as its gas token, so you only need USDT0 to transact. There is no separate gas asset to fund. :::note Prefer a typed client? The [Stable SDK](/en/explanation/sdk-overview) wraps viem with `transfer`, `bridge`, and `swap` methods so you skip the manual ABI and decimals work. ::: ### Prerequisites * Node.js 20 or later * A private key you control (a fresh test key is fine) ### 1. Install and configure Create a project, install `ethers`, and save the testnet config. ```bash mkdir stable-quickstart && cd stable-quickstart npm init -y && npm install ethers ``` ```text added 1 package, audited 2 packages in 1s ``` Save your private key to `.env`: ```bash echo "PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE" > .env ``` Create `config.ts`: ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); ``` ### 2. Fund the wallet Print your address, then request testnet USDT0 from the faucet. ```typescript // address.ts import { wallet } from "./config"; console.log("Wallet address:", wallet.address); ``` ```bash npx tsx address.ts ``` ```text Wallet address: 0x1234...abcd ``` Go to [https://faucet.stable.xyz](https://faucet.stable.xyz), paste the address, and select the button to receive testnet USDT0. The faucet sends 1 USDT0, enough for thousands of native transfers. ### 3. Send your first transaction Send 0.001 USDT0 natively. On Stable, USDT0 is the native asset, so a simple value transfer is the cheapest path (21,000 gas). ```typescript // send.ts import { ethers } from "ethers"; import { provider, wallet } from "./config"; const recipient = "0xRecipientAddress"; // replace with any address const amount = ethers.parseEther("0.001"); // 0.001 USDT0 (18 decimals, native) const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: amount, maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); const receipt = await tx.wait(1); console.log("Tx:", receipt!.hash); console.log("Explorer:", `https://testnet.stablescan.xyz/tx/${receipt!.hash}`); ``` ```bash npx tsx send.ts ``` ```text Tx: 0x8f3a...2d41 Explorer: https://testnet.stablescan.xyz/tx/0x8f3a...2d41 ``` Open the explorer link to confirm the transaction. Block time is roughly 0.7 seconds, so it should already be final. :::warning `maxPriorityFeePerGas` is ignored by Stable and must be set to `0`. See [Gas pricing](/en/reference/gas-pricing-api) for how the base-fee-only model changes transaction construction. ::: ### Where to go next * [**Deploy a smart contract**](/en/tutorial/smart-contract) — Scaffold a Foundry project and deploy to Stable testnet. * [**Build a payment app**](/en/how-to/build-p2p-payments) — Create wallet, send, receive, and query payment history. * [**Develop with AI**](/en/how-to/develop-with-ai) — Wire MCP servers and agent skills into your AI editor. ## SDK quickstart You'll install `@stablechain/sdk`, create a client signed by a private key, send a USDT0 transfer on Stable Testnet, and fetch a bridge and swap quote. Total time: about five minutes. :::note Stable uses USDT0 as the gas token. You only need testnet USDT0 to transact — there's no separate native asset to fund. ::: ### Prerequisites * Node.js 20 or later * A test private key with testnet USDT0. See [Fund your testnet wallet](/en/how-to/use-faucet). ### 1. Install ```bash mkdir stable-sdk-quickstart && cd stable-sdk-quickstart npm init -y && npm install @stablechain/sdk viem ``` ```text added 2 packages, audited 3 packages in 2s ``` Save your test key: ```bash echo "PRIVATE_KEY=0xYOUR_TEST_KEY" > .env ``` ### 2. Create a client Create `index.ts`: ```ts import "dotenv/config"; import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const stable = createStable({ network: Network.Testnet, account, }); console.log("Signer:", account.address); ``` ```text Signer: 0xYourAddress ``` `createStable` accepts three signing modes: `account` (server-side, shown above), `transport` (browser wallet via `custom(window.ethereum)`), or `walletClient` (a pre-built viem `WalletClient`). See [Use the SDK with viem](/en/how-to/sdk-with-viem) for all three. ### 3. Send a USDT0 transfer Append to `index.ts`: ```ts const { txHash } = await stable.transfer({ from: account.address, to: "0x000000000000000000000000000000000000dEaD", amount: 0.001, }); console.log("Transfer:", txHash); ``` Run it: ```bash npx tsx index.ts ``` ```text Signer: 0xYourAddress Transfer: 0x8f3a...2d41 ``` Open the hash on the [testnet explorer](https://testnet.stablescan.xyz) to confirm. ### 4. Quote a bridge Bridge USDT0 from Ethereum Sepolia to Stable Testnet. `quoteBridge` is a read-only call — no signature, no gas: ```ts import { Chain } from "@stablechain/sdk"; const bridgeQuote = await stable.quoteBridge({ fromChain: Chain.Sepolia, toChain: Chain.StableTestnet, fromToken: "0xc4DCC311c028e341fd8602D8eB89c5de94625927", toToken: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", amount: 1, }); console.log("Bridge quote:", bridgeQuote); ``` ```text Bridge quote: { toAmount: 0.999812 } ``` Pass the quote into `stable.bridge({ ...params, quote })` to execute. The SDK picks LayerZero for USDT0 → USDT0 routes and LI.FI for everything else. ### 5. Quote a swap Swaps run on Stable through LI.FI. The quote returns the expected output and a pre-built transaction: ```ts const swapQuote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", amount: 1, fromDecimals: 6, }); console.log("You'll receive:", swapQuote.toAmount, "USDT0"); ``` ```text You'll receive: 0.998 USDT0 ``` Call `stable.swap({ ...params, quote: swapQuote })` to execute. Approval for ERC-20 sources is handled internally. ### Next recommended * [**SDK reference**](/en/reference/sdk) — Every parameter, return type, and error class. * [**Use with viem**](/en/how-to/sdk-with-viem) — Switch between private-key, browser-wallet, and pre-built `WalletClient` signing. * [**Use with wagmi**](/en/how-to/sdk-with-wagmi) — Wire the SDK into a React app using wagmi hooks. ## Send your first USDT0 On Stable, USDT0 is both the chain's native asset and an ERC-20 token. This means `approve`, `transferFrom`, and `permit` remain fully available alongside standard value transfers, and both paths move funds from the same underlying balance. This page walks you through sending USDT0 through both paths and confirming they draw from one balance. :::note Prefer a typed client? The [Stable SDK](/en/explanation/sdk-overview) exposes a single `transfer({ to, amount, token? })` that covers both paths, handles decimals on-chain, and switches the wallet's chain for you. ::: :::note **18 vs 6 decimals**: Native USDT0 uses 18 decimals (standard EVM precision), while the ERC-20 interface reports 6 decimals (standard USDT precision). Both reflect the same balance, so `address(x).balance` and `USDT0.balanceOf(x)` may differ by up to 0.000001 USDT0 due to fractional reconciliation. See [USDT0 behavior on Stable](/en/explanation/usdt0-behavior). ::: ### What you'll build A two-script flow that sends 0.001 USDT0 as a native transfer, sends 0.001 USDT0 as an ERC-20 transfer, and prints both balances. #### Demo ```text step 1. Connect wallet → balance displayed 0.01 USDT0 step 2. Send 0.001 USDT0 (choose native or ERC-20 transfer) step 3. Result Sent: 0.001 USDT0 Gas fee: 0.000021 USDT0 Native balance: 0.008979 USDT0 ERC-20 balance: 0.008979 USDT0 ``` ### Prerequisites * Node.js 20 or later * A private key with testnet USDT0. See [Quick start](/en/tutorial/quick-start) to fund a wallet. **USDT0 contract addresses** * Mainnet: `0x779ded0c9e1022225f8e0630b35a9b54be713736` * Testnet: `0x78cf24370174180738c5b8e352b6d14c83a6c9a9` ### Setup ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); ``` ### Option 1 (recommended): send as native transfer Native transfers work the same as sending ETH on Ethereum. The `value` field carries the USDT0 amount. A native transfer costs only 21,000 gas, the cheapest way to send USDT0. ```typescript // sendNative.ts import { ethers } from "ethers"; import { provider, wallet } from "./config"; const recipient = "0xRecipientAddress"; const amount = ethers.parseUnits("0.001", 18); // 18 decimals for native const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: amount, maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); const receipt = await tx.wait(1); console.log("Native transfer tx:", receipt!.hash); ``` ```bash npx tsx sendNative.ts ``` ```text Native transfer tx: 0x8f3a...2d41 ``` ### Option 2: send as ERC-20 transfer USDT0 can also be sent as an ERC-20 transfer. This deducts from the same balance, but uses the ERC-20 interface with 6-decimal precision. ```typescript // sendERC20.ts import { ethers } from "ethers"; import { wallet, USDT0_ADDRESS } from "./config"; const recipient = "0xRecipientAddress"; const amount = ethers.parseUnits("0.001", 6); // 6 decimals for ERC-20 const usdt0 = new ethers.Contract(USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], wallet); const tx = await usdt0.transfer(recipient, amount); const receipt = await tx.wait(1); console.log("ERC-20 transfer tx:", receipt!.hash); ``` ```bash npx tsx sendERC20.ts ``` ```text ERC-20 transfer tx: 0xa2b1...77c0 ``` ### Verify the unified balance After either transfer, query both balances to confirm they draw from the same source. ```typescript // balances.ts import { ethers } from "ethers"; import { provider, wallet, USDT0_ADDRESS } from "./config"; const nativeBalance = await provider.getBalance(wallet.address); console.log("Native balance:", ethers.formatEther(nativeBalance), "USDT0"); const usdt0 = new ethers.Contract(USDT0_ADDRESS, [ "function balanceOf(address) view returns (uint256)" ], provider); const erc20Balance = await usdt0.balanceOf(wallet.address); console.log("ERC-20 balance:", ethers.formatUnits(erc20Balance, 6), "USDT0"); ``` ```bash npx tsx balances.ts ``` ```text Native balance: 0.008979 USDT0 ERC-20 balance: 0.008979 USDT0 ``` Both values represent the same balance. They may differ by up to 0.000001 USDT0 due to [fractional balance reconciliation](/en/explanation/usdt0-behavior#balance-reconciliation). ### Next recommended * [**Zero gas transactions**](/en/how-to/zero-gas-transactions) — Send USDT0 with gas fees paid by a waiver service. * [**Build a P2P payment app**](/en/how-to/build-p2p-payments) — Create wallet, send, receive, and query payment history. * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Understand dual-role balance reconciliation and contract design. ## Deploy a smart contract In this tutorial, you will deploy a simple smart contract to the Stable Testnet and read its state from the chain. Along the way, you will learn how Stable's network is configured, how USDT0 works as a gas token, and how to point standard EVM tooling at Stable. This tutorial assumes basic familiarity with Solidity and a Unix-like terminal. No prior Stable experience is required. ### What you'll build A fresh Foundry project with the sample `Counter` contract, deployed to Stable testnet, with one state-changing call and one read call. #### Demo ```text step 1. Scaffold Foundry project → stable-hello/ step 2. Configure testnet RPC: https://rpc.testnet.stable.xyz Chain ID: 2201 step 3. Fund wallet from faucet (1 USDT0) step 4. forge create Counter Deployed to: 0xContract... step 5. cast send Counter.setNumber(42) step 6. cast call Counter.number() → 42 ``` ### Prerequisites * [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (`forge`, `cast`, and `anvil` available in your PATH) * A wallet with a private key you control (a fresh test key is fine; never use a key holding real funds on testnet) * An internet connection to reach the testnet RPC and faucet *** ### 1. Create a new Foundry project Run the following command to scaffold a fresh project: ```bash forge init stable-hello && cd stable-hello ``` Foundry creates a `src/` directory with a sample `Counter.sol` contract and a matching test file. You will deploy this contract as-is. The goal is to get something real on-chain, not to write novel Solidity. ### 2. Review the contract you are deploying Open `src/Counter.sol`. It contains two functions: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } ``` `number` is a public state variable stored on-chain. `increment()` and `setNumber()` are the two ways to change it. Reading `number` costs no gas. It is a free `eth_call`. ### 3. Configure the Stable Testnet Create a file named `.env` at the project root to store your network credentials: ```bash touch .env ``` Add the following, replacing the placeholder with your actual private key: ```bash PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE ``` Next, open `foundry.toml` and add the Stable Testnet as a named network profile. Append this block below the existing `[profile.default]` section: ```toml [rpc_endpoints] stable_testnet = "https://rpc.testnet.stable.xyz" ``` This tells Foundry where to send transactions when you target `stable_testnet`. Stable is EVM-compatible, so no other configuration is needed. *** **Checkpoint:** Confirm your RPC endpoint is reachable: ```bash cast chain-id --rpc-url https://rpc.testnet.stable.xyz ``` Expected output: ``` 2201 ``` Chain ID `2201` is the Stable Testnet. If you see this number, your machine can reach the network. *** ### 4. Get your wallet address Derive your deployer address from your private key so you know which account to fund: ```bash source .env cast wallet address $PRIVATE_KEY ``` Copy the address that is printed. You need it in the next step. ### 5. Fund your wallet with USDT0 Stable uses **USDT0** as its gas token. The same asset you use to pay for goods and services is used directly to pay for computation. There is no secondary native token. Visit the testnet faucet and request funds: ``` https://faucet.stable.xyz ``` Paste the address from the previous step. The faucet sends 1 USDT0 to your wallet, which is enough to deploy and interact with several contracts. *** **Checkpoint:** Confirm your balance arrived: ```bash cast balance $PRIVATE_KEY --rpc-url https://rpc.testnet.stable.xyz ``` You should see a non-zero value. If the balance is still `0`, wait a few seconds and re-run. Stable produces a new block roughly every 0.7 seconds, so funds settle quickly. *** ### 6. Deploy the contract Run the deployment with `forge create`: ```bash source .env forge create src/Counter.sol:Counter \ --rpc-url https://rpc.testnet.stable.xyz \ --private-key $PRIVATE_KEY \ --broadcast ``` Foundry compiles the contract, broadcasts a deployment transaction, and waits for the receipt. Because block time is \~0.7 seconds, this takes only a moment. *** **Checkpoint:** The output should look like this: ``` [⠒] Compiling... No files changed, compilation skipped Deployer: 0xYourAddress Deployed to: 0xSomeContractAddress Transaction hash: 0xSomeTxHash ``` Copy the `Deployed to` address. You need it in the next two steps. *** ### 7. Call a write function Now call `setNumber()` to store a value on-chain: ```bash cast send 0xSomeContractAddress "setNumber(uint256)" 42 \ --rpc-url https://rpc.testnet.stable.xyz \ --private-key $PRIVATE_KEY ``` This sends a transaction. You are paying a small USDT0 fee for the state change. The value `42` is now stored in the `number` variable on the Stable Testnet. ### 8. Read state from the chain Call `number()` to read the value back. This is a free read, with no transaction and no gas: ```bash cast call 0xSomeContractAddress "number()(uint256)" \ --rpc-url https://rpc.testnet.stable.xyz ``` Expected output: ``` 42 ``` You just wrote to and read from the Stable Testnet. The round-trip — deploy, write, read — is the core loop of EVM development, and it works identically here to any other EVM chain. ### 9. Inspect your deployment on Stablescan Open the Stable Testnet block explorer and paste your contract address: ``` https://testnet.stablescan.xyz ``` You will see your deployment transaction and the `setNumber` call you made. Stablescan is the canonical tool for inspecting on-chain state, verifying contract source code, and reading transaction history on Stable. *** ### What you have built You deployed a contract, sent a state-changing transaction, and read on-chain state — all on the Stable Testnet. You now know how to: * Configure Foundry (or any EVM toolchain) to target Stable using a standard RPC endpoint * Fund a wallet using the USDT0 faucet * Pay for transactions with USDT0 as the gas token * Inspect your work on Stablescan ### Next recommended * [**Verify the contract**](/en/how-to/verify-contract) — Upload your source to Stablescan so users can read and interact with it. * [**Index contract events**](/en/how-to/index-contract) — Subscribe to events with ethers.js and backfill historical logs. * [**Gas pricing reference**](/en/reference/gas-pricing-api) — Understand how USDT0-denominated fees are calculated. ## Brand Kit The Stable brand kit includes logos in multiple formats and the official color palette. Use it to keep Stable's branding consistent across projects and communications. [Open the Stable Brand Kit](https://www.stable.xyz/brand-kit) ## Facilitators A facilitator verifies a signed x402 payment and submits the on-chain call that settles it in USDT0 on Stable. Using a hosted facilitator means you do not run settlement infrastructure or manage gas tokens. For the rail-level context, see [Agent settlement](/en/explanation/agent-settlement). ### Overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :---------------------------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | | [**Semantic Pay**](https://x402.semanticpay.io) | x402 facilitator | [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) | Public x402 facilitator for Stable; verifies and settles USDT0 payments via ERC-3009 | | [**Heurist**](https://facilitator.heurist.xyz) | x402 facilitator | [https://docs.heurist.ai/x402-products/facilitator#supported-networks](https://docs.heurist.ai/x402-products/facilitator#supported-networks) | Multi-chain x402 facilitator supporting Stable; high-throughput verification and settlement with OFAC screening | ### Semantic Pay Payment infrastructure for AI agents: trustless, P2P, and permissionless. Semantic Pay operates as an x402-compatible payment facilitator on Stable, settling USDT0 payments via ERC-3009 (`transferWithAuthorization`). Agents do not need a separate gas token in their wallets. Developers integrating x402 on Stable point their middleware to `https://x402.semanticpay.io`. No custom settlement infrastructure is required. **Capabilities** * Payment payload verification (`/verify`) and on-chain settlement (`/settle`) via USDT0 * Gasless transfers using ERC-3009 `transferWithAuthorization` * Spend limits, approval flows, and kill switches for agent oversight * End-to-end traceability with full audit logging from intent through settlement * Event callbacks for real-time payment lifecycle updates **Facilitator endpoint:** `https://x402.semanticpay.io` **Docs:** [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) ### Heurist Heurist operates a multi-chain x402 facilitator that supports Stable alongside Base, Base Sepolia, and X Layer. It targets high-frequency agent workloads with throughput tuned for thousands of payment verifications and settlements per second. Point your middleware at `https://facilitator.heurist.xyz`. No API key is required to get started. **Capabilities** * Payment verification and on-chain settlement across multiple networks from one endpoint * Throughput sized for high-frequency agent traffic * Automatic OFAC screening of sender addresses * Real-time observability over verification and settlement activity **Facilitator endpoint:** `https://facilitator.heurist.xyz` **Docs:** [https://docs.heurist.ai/x402-products/facilitator#supported-networks](https://docs.heurist.ai/x402-products/facilitator#supported-networks) ### How to choose * Use a hosted facilitator (Semantic Pay or Heurist) to start fast. No infrastructure to run, no gas tokens to manage. * Pick by what matters most for your workload: Semantic Pay if you want Stable-native tooling with lifecycle callbacks and agent oversight controls; Heurist if you need a single endpoint that spans Stable plus other EVM networks, or if OFAC screening is a hard requirement. * Self-host if you need full control over settlement policy, want to keep payment data in your own environment, or expect volume that justifies the operational overhead. * Whichever route you choose, test against a small payment first to confirm verification and settlement behave as you expect before sending production traffic. * Both facilitators settle x402 on Stable today. The same `/settle` endpoint can also serve as the on-chain submission target for an MPP server, since MPP's wire format only differs from x402 on the client ↔ resource-server hop. See [Build an MPP endpoint on Stable](/en/how-to/build-mpp-endpoint). *** Have an agentic payments integration with Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## Wallets Agent wallets give AI agents and autonomous systems self-custodial signing so they can participate in x402 payment flows without human-driven setup. ### Overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :---------------------------------------------------------------- | :--------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- | | [**Wallet Development Kit (WDK)**](https://docs.wallet.tether.io) | Agent wallet SDK | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether's open-source SDK; `WalletAccountEvm` satisfies the x402 client signer interface natively | ### Wallet Development Kit (WDK) by Tether An open-source SDK from Tether for building self-custodial AI agent wallets. WDK enables agents to generate and store private keys locally, without relying on cloud-based KMS or TEE infrastructure. The `WalletAccountEvm` instance from WDK natively satisfies the client signer interface required by the x402 SDK. An agent equipped with WDK and USDT0 on Stable can automatically intercept 402 HTTP responses, sign ERC-3009 authorizations, and resubmit requests. **Packages:** `@tetherto/wdk`, `@tetherto/wdk-wallet-evm` **Capabilities** * Self-custodial key generation and local storage * Native x402 client signer compatibility via `WalletAccountEvm` * Automatic 402 response interception and ERC-3009 signing * Multi-chain support including Stable **Docs:** [https://docs.wallet.tether.io](https://docs.wallet.tether.io) *** Have an agent wallet integration with Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## Bank precompile reference :::note **Concept:** For what the bank module does and when to use it, see [Bank module](/en/explanation/bank-module). ::: ### Abstract The `x/bank` module in Stable SDK only provides basic token management features. You can transfer any token to any account without restriction, but you cannot delegate another account to transfer your tokens. For these reasons, the `bank` precompiled contract offers additional authorization and delegation features on top of the existing `x/bank` module in Stable SDK. ### Contents 1. **[Concepts](#concepts)** 2. **[Configuration](#configuration)** 3. **[Methods](#methods)** 4. **[Events](#events)** ### Concepts This precompiled contract provides ERC-20 standard methods - such as `transfer` and `balanceOf` for transfer and `transferFrom`, `approve` and `allowance` for delegation. You can call these methods directly without registering the contract address. However, the `x/precompile` module must whitelist and register the contract address before you can use the `mint` and `burn` methods. ```go func (p *Precompile) mint( ctx sdk.Context, contract *vm.Contract, denom string, method *abi.Method, stateDB vm.StateDB, args []interface{}, ) ([]byte, error) { // ... // mint method is only allowed for the registered caller contract if _, err := precompilecommon.CheckPermissions(ctx, p.precompileKeeper, contract.CallerAddress, CallerPermissions); err != nil { return nil, err } ``` This additional verification process guarantees that the token contract calling this precompiled contract is authorized. To register a token contract address and its denom in the `x/precompile` module whitelist, you must submit a governance proposal. ### Configuration The contract address and gas cost are predefined. #### Contract address * `0x0000000000000000000000000000000000001003` for STABLE (governance token) ### Methods #### `mint` Mints requested amount of new tokens and transfer to the account. The amount of tokens to be minted must be greater than zero. `PrecompiledBankMint` is emitted when the tokens are successfully minted and transferred to the account. NOTE: * Governance token minting is prohibited. * Caller contracts calling the mint method must be registered in x/precompile module. ##### Inputs | Name | Type | Description | | ------ | ------- | ---------------------------------------- | | to | address | the address to receive the minted tokens | | amount | uint256 | the amount of tokens to be minted | ##### Outputs | Name | Type | Description | | ------- | ---- | ------------------------------------------------------------------------- | | success | bool | true if the tokens are successfully minted and transferred to the account | #### `burn` Burns requested amount of tokens from the account. The amount of tokens to be burned must be greater than zero. `PrecompiledBankBurn` is emitted when the tokens are successfully burned. NOTE: * Burning governance token is prohibited. * Caller contracts calling the burn method must be registered in x/precompile module. ##### Inputs | Name | Type | Description | | ------ | ------- | --------------------------------- | | from | address | the address to burn the tokens | | amount | uint256 | the amount of tokens to be burned | ##### Outputs | Name | Type | Description | | ------- | ---- | ------------------------------------------ | | success | bool | true if the tokens are successfully burned | #### `transfer` Transfers requested amount of tokens from sender to the recipient. Token must be set sendable. The amount of tokens to be transferred must be greater than zero. `PrecompiledBankTransfer` is emitted when the tokens are successfully transferred. ##### Inputs | Name | Type | Description | | ------ | ------- | -------------------------------------- | | to | address | the address to receive the tokens | | amount | uint256 | the amount of tokens to be transferred | ##### Outputs | Name | Type | Description | | ------- | ---- | ----------------------------------------------- | | success | bool | true if the tokens are successfully transferred | #### `transferFrom` Transfers requested amount of tokens from owner to recipient by authorized spender within the limits of the allowance. Token must be set sendable. The amount of tokens to be transferred must be greater than zero and less than or equal to the current allowance. `PrecompiledBankTransfer` is emitted when the tokens are successfully transferred. ##### Inputs | Name | Type | Description | | ------ | ------- | -------------------------------------- | | from | address | the address to transfer the tokens | | to | address | the address to receive the tokens | | amount | uint256 | the amount of tokens to be transferred | ##### Outputs | Name | Type | Description | | ------- | ---- | ----------------------------------------------- | | success | bool | true if the tokens are successfully transferred | #### `multiTransfer` Transfers tokens from single account to multiple accounts. Token must be set sendable. The amount of tokens to be transferred to each recipient must be greater than zero. `PrecompiledBankTransfer` is emitted per each recipient when the tokens are successfully transferred. ##### Inputs | Name | Type | Description | | ------ | ---------- | -------------------------------------------------------- | | to | address\[] | the addresses to receive the transferred tokens | | amount | uint256\[] | the amount of tokens to be transferred to each recipient | ##### Outputs | Name | Type | Description | | ------- | ---- | ----------------------------------------------------------------- | | success | bool | true if the tokens are successfully transferred to each recipient | #### `approve` Authorizes a spender to transfer tokens from the owner’s account. The amount of tokens to be authorized must be greater than zero. `PrecompiledBankApproval` is emitted when the authorization is successfully set. ##### Inputs | Name | Type | Description | | ------- | ------- | ------------------------------------- | | spender | address | the address to authorize | | value | uint256 | the amount of tokens to be authorized | ##### Outputs | Name | Type | Description | | ------- | ---- | --------------------------------------------- | | success | bool | true if the authorization is successfully set | #### `revoke` Revokes the authorization of spender for transferring tokens from owner. `PrecompiledBankRevoke` is emitted when the authorization is successfully revoked. ##### Inputs | Name | Type | Description | | ------- | ------- | --------------------- | | spender | address | the address to revoke | ##### Outputs | Name | Type | Description | | ------- | ---- | ------------------------------------------------- | | success | bool | true if the authorization is successfully revoked | #### `balanceOf` Returns balance of tokens from the account. ##### Inputs | Name | Type | Description | | ------- | ------- | ---------------------------------------- | | account | address | the address to get the balance of tokens | ##### Outputs | Name | Type | Description | | ------- | ------- | ----------------------------------- | | balance | uint256 | the amount of tokens in the account | #### `totalSupply` Returns total supply of tokens. ##### Inputs none ##### Outputs | Name | Type | Description | | ----------- | ------- | -------------------------- | | totalSupply | uint256 | the total amount of tokens | #### `allowance` Returns the amount which spender is still allowed to withdraw from owner. ##### Inputs | Name | Type | Description | | ------- | ------- | -------------------------- | | owner | address | the address of the owner | | spender | address | the address of the spender | ##### Outputs | Name | Type | Description | | ------ | ------- | ------------------------------- | | amount | uint256 | the amount of tokens authorized | ### Events All events emitted from this precompiled contract are prefixed with `PrecompiledBank`. To avoid ambiguity, token contract calling this precompiled contract should avoid using event names with the same prefix. #### PrecompiledBankMint | Name | Type | Indexed | Description | | ------ | ------- | ------- | ---------------------------------------- | | from | address | Y | the address that minted the tokens | | to | address | Y | the address to receive the minted tokens | | amount | uint256 | N | the amount of tokens minted | #### PrecompiledBankBurn | Name | Type | Indexed | Description | | ------ | ------- | ------- | ---------------------------------- | | from | address | Y | the address that burned the tokens | | to | address | Y | not used in this method | | amount | uint256 | N | the amount of tokens burned | #### PrecompiledBankTransfer | Name | Type | Indexed | Description | | ------ | ------- | ------- | --------------------------------------------- | | from | address | Y | the address that transferred the tokens | | to | address | Y | the address to receive the transferred tokens | | amount | uint256 | N | the amount of tokens transferred | #### PrecompiledBankApproval | Name | Type | Indexed | Description | | ------- | ------- | ------- | -------------------------------------- | | owner | address | Y | the address that authorized the tokens | | spender | address | Y | the address to authorize | | value | uint256 | N | the amount of tokens authorized | #### PrecompiledBankRevoke | Name | Type | Indexed | Description | | ------- | ------- | ------- | ----------------------------------- | | owner | address | Y | the address that revoked the tokens | | spender | address | Y | the address to revoke | | value | uint256 | N | the amount of tokens authorized | ## Bridges Bridge providers supporting USDT0 transfers to and from Stable. For how cross-chain USDT0 movement works, see [Bridging to Stable](/en/explanation/usdt0-bridging). For a hands-on walkthrough, see the [Bridge USDT0 to Stable Testnet](/en/tutorial/bridge-usdt0) tutorial. *** ### Supported source chains Any chain with USDT0 can bridge to Stable via the OFT Mesh. Any chain with native USDT can route through the Legacy Mesh via the Arbitrum hub. Current participants: | Path | Example chains | Mechanism | Fee | | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------- | :----------------------- | | **OFT Mesh** | Arbitrum, Bera, Conflux, Ethereum, Flare, Hedera, Hyperliquid, Ink, Mantle, MegaETH, Monad, Morph, MP1, Optimism, Plasma, Polygon, Rootstock, Sei, Tempo, Unichain, X Layer | Burn on source, mint on Stable | Source chain gas only | | **Legacy Mesh** | Tron, TON | Lock native USDT → Arbitrum hub → mint USDT0 on Stable | 0.03% + source chain gas | Ethereum and Arbitrum support both paths: users holding native USDT can use the Legacy Mesh, while users holding USDT0 can use the OFT Mesh directly. *** ### Contract addresses | | Testnet (chain ID 2201) | Mainnet (chain ID 988) | | :-------------------------------- | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | | **LayerZero EID** | `40374` | See [LayerZero deployed contracts](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable) | | **LayerZero Endpoint V2** | `0x3aCAAf60502791D199a5a5F0B173D78229eBFe32` | See LayerZero docs | | **USDT0 token** | `0x78Cf24370174180738C5B8E352B6D14c83a6c9A9` | `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` | | **USDT0 OApp (on Stable)** | N/A | `0xedaba024be4d87974d5aB11C6Dd586963CcCB027` | | **Source USDT0 (Sepolia)** | `0xc4DCC311c028e341fd8602D8eB89c5de94625927` | Use mainnet USDT0 on source chain | | **Source OApp (Sepolia)** | `0xc099cD946d5efCC35A99D64E808c1430cEf08126` | Use mainnet OApp on source chain | | **LiFi Diamond (Stable mainnet)** | N/A | `0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37` | For the full testnet contract list (LayerZero endpoint, DVN, executor), see [Testnet ecosystem contracts](/en/reference/testnet-ecosystem). *** ### STABLE OFT contracts The STABLE token bridges to other chains using the LayerZero OFT standard. The adapter on Stable locks STABLE for outbound transfers; the upgradeable proxy on each remote chain mints and burns the wrapped supply. For the security model and a description of each contract's role, see [Bridge security and DVNs](/en/explanation/bridge-security). | Chain | Contract | Address | | :----------- | :----------------------------- | :------------------------------------------- | | **Stable** | `StableOFTAdapter` | `0x386f92606b2D5E0A992ECc3704c31eF39Ff56392` | | **BSC** | `StableOFTUpgradeable` (proxy) | `0x011EBe7d75E2C9D1E0bD0be0bEf5C36f0A90075F` | | **HyperEVM** | `StableOFTUpgradeable` (proxy) | `0xa51dC81944a15623874981181a99D6c56B20ED56` | :::note The remote chain addresses differ even though both are EVM-compatible. Activating the destination address on HyperCore requires a transaction, which advances the deploy nonce and produces a different deterministic address than BSC. See [stablelabs/chain-oft](https://github.com/stablelabs/chain-oft) for the canonical config. ::: *** ### Stable's DVN operators Stable's bridges run a 3/3 required DVN configuration: three independent operators must each sign every cross-chain message before it is accepted. There is no optional pool. The three required signers and their DVN contract addresses: | Operator | DVN address | | :----------------- | :------------------------------------------- | | **LayerZero Labs** | `0x9c061c9a4782294eef65ef28cb88233a987f4bdd` | | **Canary** | `0x8d6cc20d84fbeb5733c60436ceb8957da2ac02c8` | | **Horizen** | `0x965a80dc87cec5848310e612dead84b543aef874` | For the on-chain config per pathway, see [LayerZero deployed contracts](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable). For the security rationale, see [Bridge security and DVNs](/en/explanation/bridge-security). *** ### Bridge providers | Provider | Type | Status | Description | Docs | | :------------------------------------------------------------------ | :--------------------------------- | :---------------------- | :----------------------------------------------------------- | :--------------------------------------------------------------------------- | | **[LayerZero](https://docs.layerzero.network/v2)** | Cross-chain messaging (OFT) | Live | Powers USDT0 OFT burn/mint transfers; dual DVN verification | [docs.layerzero.network/v2](https://docs.layerzero.network/v2) | | **[Stargate](https://docs.stargate.finance/introduction/overview)** | Direct bridge (liquidity pools) | Live | Unified liquidity pools; stablecoin-optimized routing | [docs.stargate.finance](https://docs.stargate.finance/introduction/overview) | | **[Gas.Zip](https://dev.gas.zip/overview)** | Direct bridge (liquidity routing) | Live | Liquidity routing across 350+ chains; fast finality | [dev.gas.zip](https://dev.gas.zip/overview) | | **[LiFi](https://docs.li.fi/api-reference/introduction)** | Bridge aggregator | Live | Routes across multiple bridges and DEX swaps; SDK + REST API | [docs.li.fi](https://docs.li.fi/api-reference/introduction) | | **[Polymer](https://docs.polymerlabs.org/docs/build/start/)** | Cross-chain interoperability (IBC) | Integration in progress | IBC-based messaging for Ethereum-native chains | [docs.polymerlabs.org](https://docs.polymerlabs.org/docs/build/start/) | | **[Relay](https://docs.relay.link/what-is-relay)** | Intent-based bridge | Integration in progress | Gasless execution via solver network | [docs.relay.link](https://docs.relay.link/what-is-relay) | #### LayerZero Cross-chain messaging protocol powering USDT0 OFT burn/mint transfers with dual DVN verification. **Capabilities** * OFT-standard burn-on-source, mint-on-destination transfers * Dual DVN (Decentralized Verifier Network) message verification * Powers both OFT Mesh and Legacy Mesh paths #### Stargate Liquidity pool-based bridge optimized for stablecoin routing. **Capabilities** * Unified liquidity pools across chains * Stablecoin-optimized routing * Instant guaranteed finality #### Gas.Zip Liquidity routing protocol supporting fast transfers across 350+ chains. **Capabilities** * Cross-chain liquidity routing * Fast finality * Broad chain coverage (350+ chains) #### LiFi Bridge aggregator routing transfers across multiple bridges and DEX swaps. **Capabilities** * Multi-bridge route optimization * SDK and REST API integration * DEX swap aggregation #### Polymer IBC-based cross-chain messaging for Ethereum-native chains. Integration in progress. **Capabilities** * IBC protocol messaging on Ethereum * Native interoperability without external validators #### Relay Intent-based bridge with gasless execution via a solver network. Integration in progress. **Capabilities** * Intent-based bridging * Gasless execution for users * Solver network settlement *** ### Fee structure | Provider | Fee model | | :-------------------------- | :-------------------------------------------------------------------------------------------------- | | **LayerZero (OFT Mesh)** | Source chain gas only (no protocol fee) | | **LayerZero (Legacy Mesh)** | 0.03% of transferred amount (charged by USDT0 team) + source chain gas | | **Stargate** | Liquidity pool fees apply; see [Stargate docs](https://docs.stargate.finance/introduction/overview) | | **LiFi** | Aggregator routing fee may apply depending on path | | **Gas.Zip** | See [Gas.Zip docs](https://dev.gas.zip/overview) for current fee schedule | | **Relay** | Solver fees; see [Relay docs](https://docs.relay.link/what-is-relay) | *** Have a bridge integrating Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## Connect This page consolidates the network details you need to connect to Stable. ### Mainnet | **Field** | **Value** | | :-------------- | :----------------------------------------------- | | Network Name | Stable Mainnet | | Chain ID | `988` | | Currency Symbol | USDT0 | | EVM JSON-RPC | `https://rpc.stable.xyz` | | WebSocket | `wss://rpc.stable.xyz` | | Block Explorer | [https://stablescan.xyz](https://stablescan.xyz) | ### Testnet | **Field** | **Value** | | :-------------- | :--------------------------------------------------------------- | | Network Name | Stable Testnet | | Chain ID | `2201` | | Currency Symbol | USDT0 | | EVM JSON-RPC | `https://rpc.testnet.stable.xyz` | | WebSocket | `wss://rpc.testnet.stable.xyz` | | Block Explorer | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) | For third-party RPC providers, see [RPC Providers](/en/reference/rpc-providers). For a typed client that wires these endpoints in for you, see the [Stable SDK](/en/explanation/sdk-overview). ### Rate limits The public RPC endpoints (`https://rpc.stable.xyz` and `https://rpc.testnet.stable.xyz`) are rate-limited to **1,000 requests per 10 seconds per IP**. Requests over the limit return `HTTP 429`. For higher throughput, use a [third-party RPC provider](/en/reference/rpc-providers). :::note USDT0 uses **18 decimals** as the native gas token (returned by `address(x).balance`) and **6 decimals** as an ERC-20 token (returned by `USDT0.balanceOf(x)`). Both interfaces operate on the same underlying balance. Libraries like viem and ethers.js report 18 decimals because they read the native gas token. For details on how the precision gap is reconciled, see [USDT0 Behavior on Stable](/en/explanation/usdt0-behavior). ::: ### Add Stable to your wallet To add Stable manually, open your browser wallet's network settings and enter the values from the tables above. The required fields are: * **Network Name** * **RPC URL** (the EVM JSON-RPC endpoint) * **Chain ID** * **Currency Symbol**: `USDT0` ### Verify connectivity Confirm your RPC endpoint is reachable by querying the chain ID: ```bash cast chain-id --rpc-url https://rpc.stable.xyz ``` Expected output: ```text 988 ``` For the testnet: ```bash cast chain-id --rpc-url https://rpc.testnet.stable.xyz ``` Expected output: ```text 2201 ``` ### Next recommended * [**Quick start**](/en/tutorial/quick-start) — Send your first testnet transaction in five minutes. * [**Get testnet USDT0**](/en/how-to/use-faucet) — Fund a wallet from the faucet or bridge from Sepolia. * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Understand the 18/6-decimal dual role before you code against balances. ## Custody ### Custody overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :---------------------------------------- | :------------------------------ | :----------------------------------------------------------------------------------------------------- | :----------------------------------------- | | [Paxos](https://paxos.com/) | MPC custody infrastructure | [https://docs.paxos.com/guides/developer/account](https://docs.paxos.com/guides/developer/account) | Trusted by Mastercard and PayPal | | [Fireblocks](https://www.fireblocks.com/) | MPC custody infrastructure | [https://developers.fireblocks.com/docs/quickstart](https://developers.fireblocks.com/docs/quickstart) | Treasury and settlement workflows | | [Fordefi](https://www.fordefi.com/) | MPC custody infrastructure | [https://docs.fordefi.com/](https://docs.fordefi.com/) | Policy engine and developer APIs | | [Anchorage](https://www.anchorage.com/) | Regulated institutional custody | [https://www.anchorage.com/get-in-touch](https://www.anchorage.com/get-in-touch) | Federally chartered bank; $45B+ in custody | ### Category guide * **MPC custody infrastructure:** Platforms using multi-party computation to distribute private key control across multiple parties. These provide secure key management, policy engines, and developer APIs for institutional digital asset operations. * **Regulated institutional custody:** Federally regulated bank-grade custody for institutions that require a chartered custodian with direct regulatory oversight. ### MPC custody infrastructure #### [Paxos](https://paxos.com/) A regulated blockchain and tokenization infrastructure platform trusted by global enterprises including Mastercard and PayPal. **Capabilities** * Regulated custody and settlement infrastructure * Enterprise-grade asset safekeeping * Tokenization services for institutions * Compliance frameworks for stablecoin operations **Get started**: Create a developer account and follow the [Paxos developer onboarding guide](https://docs.paxos.com/guides/developer/account) to configure custody and settlement for Stable assets. #### [Fireblocks](https://www.fireblocks.com/) Financial infrastructure that powers custody, treasury, and digital asset operations for institutions worldwide. **Capabilities** * MPC-based digital asset custody * Secure transfer and treasury workflows * Institutional settlement network * Stablecoin program infrastructure **Get started**: Follow the [Fireblocks quickstart guide](https://developers.fireblocks.com/docs/quickstart) to set up a workspace, configure Stable as a supported network, and begin managing digital assets. #### [Fordefi](https://www.fordefi.com/) An institutional MPC wallet and security platform built for decentralized finance. Fordefi provides key management, policy controls, and developer APIs for Web3 institutions. **Capabilities** * MPC-based distributed key generation and threshold signing * Institutional policy engine and approval workflows * Developer APIs for programmatic wallet operations * Browser extension, mobile, and API interfaces **Get started**: Review the [Fordefi developer documentation](https://docs.fordefi.com/) to create a vault, configure approval policies, and connect to Stable via the API. ### Regulated institutional custody #### [Anchorage](https://www.anchorage.com/) A federally chartered national bank providing secure, regulated custody for over $45B in digital assets. **Capabilities** * Bank-grade digital asset custody * Enterprise access controls * Regulated institutional operations * Auditable, compliant asset storage **Get started**: Contact Anchorage through their [institutional onboarding page](https://www.anchorage.com/get-in-touch) to begin the account setup process for regulated custody of Stable assets. *** Have custody infrastructure integrating with Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## Developer assistance ### FAQ A growing collection of developer-focused questions covering topics such as: * How do I connect to the Stable network? * You can interact with the network using standard JSON-RPC requests compatible with common EVM tooling. * What currency is used for transaction fees? * You pay transaction fees in USDT0. No additional fee parameters are required beyond the standard base gas price. * Where can I track updates? * The Release & Change Log communicates all protocol and developer-facing changes. * Does Stable support account abstraction? * Yes. EIP-7702 enables EOAs to temporarily operate with smart-account behavior. * See [EIP-7702 reference](/en/reference/eip-7702-api) and [Account abstraction how-to](/en/how-to/account-abstraction). * Where can I see my transaction results? * Once included in a block, results are visible through: * balance reads * contract state queries * logs and emitted events * How do I build smart contracts for Stable? * You can use standard EVM developer workflows such as: * Solidity-based contracts * JSON-RPC libraries to interact with the network This page will expand as common questions arise during public testnet usage. ### Support channels You can engage directly with the Stable team for technical assistance. * **Discord**: Join the developer channel at [https://discord.gg/stablexyz](https://discord.gg/stablexyz) * **Issue Reporting**: Instructions will be provided once public repositories open Support contacts will be updated as community platforms become available. ### Next recommended * [**Quick start**](/en/tutorial/quick-start) — Run your first testnet transaction in five minutes. * [**Production readiness**](/en/how-to/production-readiness) — Validate an integration before shipping to mainnet. * [**FAQ**](/en/reference/faq) — Common answers about chain IDs, endpoints, and onboarding. ## DEXes DEX deployments on Stable for spot trading, liquidity provision, and on-chain routing. Stable is on the [Official Uniswap v3 Deployments List](https://gov.uniswap.org/t/official-uniswap-v3-deployments-list/24323/13#p-58106-stable-4): the Uniswap v3 contracts on Stable are governance-recognized as canonical and are routed through [Stable Swap](https://swap.stable.xyz) as the default frontend. ### Overview table | **Provider** | **Category** | **Status** | **Docs / Get Started** | **Notes** | | :---------------------------------------- | :------------------------- | :-------------------------- | :----------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | | [**Uniswap v3**](https://swap.stable.xyz) | Concentrated-liquidity AMM | Canonical (live on mainnet) | [docs.uniswap.org](https://docs.uniswap.org/contracts/v3/overview) | Recognized on the Official Uniswap v3 Deployments List on May 12, 2026. Frontend: Stable Swap. Deployer: Protofire. | ### Uniswap v3 Canonical Uniswap v3 deployment on Stable, with concentrated-liquidity pools and standard fee tiers. Stable Swap is the actively maintained default frontend; trades route through the contracts below. Cross-chain liquidity flows in via LayerZero. **Capabilities** * Concentrated-liquidity AMM with v3 position NFTs * Standard `SwapRouter02`, `Quoter V2`, and `Universal Router` integration paths * `Permit2` support for gasless approvals * v2-style constant-product pools also deployed for legacy routing #### Mainnet contract addresses Source: [RFC: Stable Application for Canonical Uniswap v3 Deployment](https://gov.uniswap.org/t/rfc-stable-application-for-canonical-uniswap-v3-deployment/26080) and the [Official Uniswap v3 Deployments List](https://gov.uniswap.org/t/official-uniswap-v3-deployments-list/24323/13#p-58106-stable-4). | **Contract** | **Address** | | :----------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | | **v3 Core Factory** | [0x88F0a512eF09175D456bc9547f914f48C013E4aA](https://stablescan.xyz/address/0x88F0a512eF09175D456bc9547f914f48C013E4aA) | | **Universal Router** | [0x5Be52b52f3d1dbC324d2959637471a4208626144](https://stablescan.xyz/address/0x5Be52b52f3d1dbC324d2959637471a4208626144) | | **Swap Router02** | [0x32eaf9B5d5F2CD7361c5012890C943D7de84C22a](https://stablescan.xyz/address/0x32eaf9B5d5F2CD7361c5012890C943D7de84C22a) | | **Quoter V2** | [0xb070179E7032CdA868b53e6C1742F80c9e940d1A](https://stablescan.xyz/address/0xb070179E7032CdA868b53e6C1742F80c9e940d1A) | | **Nonfungible Token Position Manager** | [0x3BdC3437405f7D801b6036532713fc1F179136a6](https://stablescan.xyz/address/0x3BdC3437405f7D801b6036532713fc1F179136a6) | | **Nonfungible Token Position Descriptor V1.3.0** | [0x7Cf5987951E48ADf235cc9194bCdc708Eb692D82](https://stablescan.xyz/address/0x7Cf5987951E48ADf235cc9194bCdc708Eb692D82) | | **NFT Descriptor Library V1.3.0** | [0xF7815833076D83161414A46c4E993dC8f22A7ADd](https://stablescan.xyz/address/0xF7815833076D83161414A46c4E993dC8f22A7ADd) | | **Descriptor Proxy** | [0xcd2cD0E139eC5581138E18C6DBB189c53efBAE95](https://stablescan.xyz/address/0xcd2cD0E139eC5581138E18C6DBB189c53efBAE95) | | **Proxy Admin** | [0x51D1E70B8cAbDF4F3aB056475802AB1687b3EA23](https://stablescan.xyz/address/0x51D1E70B8cAbDF4F3aB056475802AB1687b3EA23) | | **Tick Lens** | [0x8dF0D1614aae99352045c62d24d54E72b38111ec](https://stablescan.xyz/address/0x8dF0D1614aae99352045c62d24d54E72b38111ec) | | **v3 Migrator** | [0x2C5f4275F1a278BF328D56CB9db304e915DE3082](https://stablescan.xyz/address/0x2C5f4275F1a278BF328D56CB9db304e915DE3082) | | **v3 Staker** | [0xA32e3E127FF46db40ab3c4775be97ED760AD7178](https://stablescan.xyz/address/0xA32e3E127FF46db40ab3c4775be97ED760AD7178) | | **Permit2** | [0x000000000022D473030F116dDEE9F6B43aC78BA3](https://stablescan.xyz/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | | **Multicall 2** | [0x208099D6E8a107aD485CD1374A6EC5Abd98c7F11](https://stablescan.xyz/address/0x208099D6E8a107aD485CD1374A6EC5Abd98c7F11) | | **V2 Core Factory** | [0x25D2d657F539F2bB16eC82773cBE5ee49ddD3c69](https://stablescan.xyz/address/0x25D2d657F539F2bB16eC82773cBE5ee49ddD3c69) | | **Uniswap V2 Router02** | [0xa571dc7c4f2369F1cA24D3a7E8a35c07Ff52bfC0](https://stablescan.xyz/address/0xa571dc7c4f2369F1cA24D3a7E8a35c07Ff52bfC0) | :::note Stable is recognized on the Uniswap v3 Deployments List as of May 12, 2026, following completion of the UAC governance process in April 2026. The deployment is maintained by Protofire with bridge connectivity via LayerZero. ::: #### Quoting a swap `Quoter V2` returns the expected output for a given input without executing a trade. Use it from any EVM tooling pointed at Stable's RPC. ```bash cast call 0xb070179E7032CdA868b53e6C1742F80c9e940d1A \ "quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256)" \ "(,,,,0)" \ --rpc-url https://rpc.stable.xyz ``` ```text (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) ``` Replace ``, ``, ``, and `` (one of `100`, `500`, `3000`, `10000`) with the values for the pool you're quoting. For application integration, prefer the [Uniswap v3 SDK](https://docs.uniswap.org/sdk/v3/overview) or the [Universal Router](https://docs.uniswap.org/contracts/universal-router/overview) pointed at the addresses above. *** ### Have a DEX integrating Stable? Reach the team at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) to be listed on this page. ### Next recommended * [Connect to Stable](/en/reference/connect): chain IDs, RPC endpoints, and block explorers for mainnet and testnet. * [Bridges](/en/reference/bridges): move USDT0 and other assets into Stable to provide liquidity or route trades. * [Oracles](/en/reference/oracles): price feeds you can use alongside swap quotes for pricing and liquidations. ## Distribution precompile reference :::note **Concept:** For what the distribution module does and when to use it, see [Distribution module](/en/explanation/distribution-module). ::: ### Abstract The `distribution` precompiled contract acts as a bridge that enables EVM environments to use the Stable SDK's `x/distribution` module functionality. ### Contents 1. **[Concepts](#concepts)** 2. **[Configuration](#configuration)** 3. **[Methods](#methods)** 4. **[Events](#events)** ### Concepts The `distribution` precompiled contract performs additional checks to ensure that the delegator or depositor is the caller. ### Configuration The contract address and gas cost are predefined. #### Contract address * `0x0000000000000000000000000000000000000801` ### Methods #### `setWithdrawAddress` Sets the address to receive the reward for the token delegated by the delegator to the validator. Sometimes, when the delegator is self-delegated, the validator address is used as the delegator. `SetWithdrawAddress` is emitted when the withdrawer address is successfully set. ##### Inputs | Name | Type | Description | | ----------------- | ------- | ----------------------------------------------- | | delegatorAddress | address | the address of the delegator | | withdrawerAddress | address | the address to receive the reward of delegation | ##### Outputs | Name | Type | Description | | ------- | ---- | -------------------------------------------------- | | success | bool | true if the withdrawer address is successfully set | #### `withdrawDelegatorRewards` Withdraws the reward to be received by the delegator from the validator. All types of tokens that the validator rewards to the delegator are withdrawn in a single transaction. `WithdrawDelegatorRewards` is emitted when the reward is successfully withdrawn. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | delegatorAddress | address | the address of the delegator | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ------ | ------- | --------------------------------------------------------- | | amount | Coin\[] | rewards of various tokens to be received by the delegator | `Coin` is a struct with the following fields: | Name | Type | Description | | ------ | ------- | ------------------------ | | denom | string | the denom of the reward | | amount | uint256 | the amount of the reward | #### `withdrawValidatorCommission` Withdraws the commission of the validator. All types of tokens that the validator receives as commission are withdrawn in a single transaction. `WithdrawValidatorCommission` is emitted when the commission is successfully withdrawn. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ------ | ------- | ------------------------------------------------------------- | | amount | Coin\[] | commissions of various tokens to be received by the validator | #### `validatorDistributionInfo` Returns the distribution information representing the reward the validator will receive. A validator can delegate tokens to itself at its own address to act as a delegator, called self-bonded. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ---------------- | ------------------------- | ----------------------------------------- | | distributionInfo | ValidatorDistributionInfo | distribution information of the validator | `ValidatorDistributionInfo` is a struct with the following fields: | Name | Type | Description | | --------------- | ---------- | -------------------------------------------- | | operatorAddress | address | the address of the operator of the validator | | selfBondRewards | DecCoin\[] | the self-bonded amount of the validator | | commission | DecCoin\[] | the commission of the validator | `DecCoin` is a struct with the following fields: | Name | Type | Description | | --------- | ------- | --------------------------- | | denom | string | the denom of the reward | | amount | uint256 | the amount of the reward | | precision | uint8 | the precision of the reward | #### `validatorOutstandingRewards` Returns the outstanding rewards of the validator. Outstanding rewards represent the total reward pool: the validator's commission and self-bonded rewards, plus the total rewards owed to delegators. For example, if validator A has delegators B, C, and D, the outstanding rewards equal A's commission and self-bonded rewards, plus the rewards of B, C, and D. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ------- | ---------- | ------------------------------------ | | rewards | DecCoin\[] | outstanding rewards of the validator | #### `validatorCommission` Returns the commission of the validator. This method is used to retrieve the commission of the validator before calling `withdrawValidatorCommission` method. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ---------- | ---------- | --------------------------- | | commission | DecCoin\[] | commission of the validator | #### `validatorSlashes` Returns the history of slashes of the validator between the starting height and ending height. Slashing is the fines imposed when a validator behaves maliciously or violates network rules such as double signing, misbehavior, or not following the chain rules. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | validatorAddress | address | the address of the validator | | startingHeight | uint64 | the starting height | | endingHeight | uint64 | the ending height | | pageRequest | PageReq | the pagination request | `PageReq` is a struct with the following fields: | Name | Type | Description | | ---------- | ------ | ------------------------------------------ | | key | bytes | the key of the pagination | | offset | uint64 | the offset of the pagination | | limit | uint64 | the limit of the pagination | | countTotal | bool | whether to count the total number of pages | | reverse | bool | whether to reverse the pagination | ##### Outputs | Name | Type | Description | | ---------- | ---------------------- | ------------------------ | | slashes | ValidatorSlashEvent\[] | slashes of the validator | | pagination | PageResp | the pagination response | `ValidatorSlashEvent` is a struct with the following fields: | Name | Type | Description | | --------------- | ------ | --------------------------- | | validatorPeriod | uint64 | the period of the validator | | fraction | Dec | the fraction of the slash | `Dec` is a struct with the following fields: | Name | Type | Description | | --------- | ------ | ------------------------ | | value | uint64 | the value of the Dec | | precision | uint8 | the precision of the Dec | `PageResp` is a struct with the following fields: | Name | Type | Description | | ------- | ------ | ------------------------------ | | nextKey | bytes | the next key of the pagination | | total | uint64 | the total number of pages | #### `delegationRewards` Returns the rewards that delegator receives from the validator. ##### Inputs | Name | Type | Description | | ---------------- | ------- | -------------------------------- | | delegatorAddress | address | the hex address of the delegator | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ------- | ---------- | -------------------------------------------------- | | rewards | DecCoin\[] | rewards that delegator receives from the validator | #### `delegationTotalRewards` Returns the total rewards that delegator receives from all validators. ##### Inputs | Name | Type | Description | | ---------------- | ------- | -------------------------------- | | delegatorAddress | address | the hex address of the delegator | ##### Outputs | Name | Type | Description | | ------- | ---------------------------- | --------------------------------------------------------- | | rewards | DelegationDelegatorReward\[] | total rewards that delegator receives from all validators | | total | DecCoin\[] | the total amount of the rewards | `DelegationDelegatorReward` is a struct with the following fields: | Name | Type | Description | | ---------------- | ---------- | -------------------------------------------------- | | validatorAddress | address | the address of the validator | | reward | DecCoin\[] | rewards that delegator receives from the validator | #### `delegatorValidators` Returns the validators that delegator is bonded to. ##### Inputs | Name | Type | Description | | ---------------- | ------- | -------------------------------- | | delegatorAddress | address | the hex address of the delegator | ##### Outputs | Name | Type | Description | | ---------- | --------- | -------------------------------------- | | validators | string\[] | validators that delegator is bonded to | #### `delegatorWithdrawAddress` Returns the address to receive the reward of delegation set by `setWithdrawAddress` method. ##### Inputs | Name | Type | Description | | ---------------- | ------- | -------------------------------- | | delegatorAddress | address | the hex address of the delegator | ##### Outputs | Name | Type | Description | | --------------- | ------- | ----------------------------------------------- | | withdrawAddress | address | the address to receive the reward of delegation | ### Events #### SetWithdrawAddress | Name | Type | Indexed | Description | | --------------- | ------- | ------- | ----------------------------------------------- | | caller | address | Y | the address of the caller (delegator) | | withdrawAddress | address | N | the address to receive the reward of delegation | #### WithdrawDelegatorRewards | Name | Type | Indexed | Description | | ---------------- | ------- | ------- | ---------------------------- | | delegatorAddress | address | Y | the address of the delegator | | validatorAddress | address | Y | the address of the validator | | amount | uint256 | N | the amount of the reward | #### WithdrawValidatorCommission | Name | Type | Indexed | Description | | ---------------- | ------- | ------- | ---------------------------------- | | validatorAddress | address | Y | the address of the validator | | commission | uint256 | N | the total amount of the commission | ## EIP-7702 Stable supports **EIP-7702**, which allows an EOA to set its account code to an existing smart contract. EOAs keep their original address and private key while executing the delegate's logic. :::note **Concept:** For what EIP-7702 enables on Stable, the delegation model, and security considerations, see [EIP-7702](/en/explanation/eip-7702). For the full specification, see the [EIP-7702 spec](https://eips.ethereum.org/EIPS/eip-7702). ::: ### Transaction format EIP-7702 uses transaction type `0x04` with an `authorizationList` field. Each authorization designates a delegate contract whose code the EOA executes for that transaction. ```typescript { type: 4, to: eoa.address, data: delegateCallData, authorizationList: [signedAuthorization], maxPriorityFeePerGas: 0n, // always 0 on Stable // ... standard EIP-1559 fields } ``` The authorization carries: * `chainId`: must match the target chain. * `address`: the delegate contract address. * `nonce`: the authorization nonce (separate from the transaction nonce). Wallets and libraries that support EIP-7702 handle the authorization format automatically. ### Tooling * **ethers.js**: `wallet.signAuthorization({ chainId, address, nonce })` produces the signed authorization for inclusion in the `authorizationList`. * **viem**: use `signAuthorization` with a walletClient, then pass the result to `sendTransaction`. * **Hardhat / Foundry**: standard EIP-7702 transaction format works when your toolchain version supports the Pectra hardfork. ### Next recommended * [**EIP-7702 concept**](/en/explanation/eip-7702) — Understand the delegation model and when to use it. * [**Account Abstraction (EIP-7702)**](/en/reference/eip-7702-api) — Implement batch payments, spending limits, and session keys step by step. ## FAQ ### Getting started **What is Stable?** A Layer 1 where USDT0 is the native gas token and settlement asset. Standard EVM tooling works unchanged. **Who is Stable for?** Three builder profiles: payment and wallet teams moving USDT0, smart contract developers deploying to an EVM, and infrastructure teams running nodes or RPC. The [Learn overview](/en/explanation/learn-overview) has a card per path. **Where should I start: Payments, Contracts, AI/Agents, or Infrastructure?** * Moving USDT0 or building payment flows → [Payments](/en/explanation/payments-overview). * Deploying contracts → [Contracts](/en/explanation/contracts-overview). * Wiring AI editors or building agent-paid services → [Agent settlement](/en/explanation/agent-settlement). * Running nodes or covering gas for users → [Infrastructure](/en/explanation/integrate-overview). If you haven't connected to testnet yet, start with [Quick start](/en/tutorial/quick-start). ### Technical **Is Stable EVM-compatible?** Yes. Solidity, Vyper, Foundry, Hardhat, ethers, viem, and the `eth_*` JSON-RPC methods all work unchanged. Four behaviors differ from Ethereum — see [Differences from Ethereum](/en/explanation/ethereum-comparison). **Why is USDT0 the gas token?** So you pay fees in the asset you're already transacting in. No second token to fund, and fees stay denominated in a stablecoin. The protocol also optimizes USDT0-heavy workloads through guaranteed blockspace and transfer aggregation; see [Core concepts](/en/explanation/core-concepts). **How do I get USDT0?** * **Testnet:** use the faucet at [faucet.stable.xyz](https://faucet.stable.xyz), or bridge Test USDT from Ethereum Sepolia. Walkthrough in [Get testnet USDT0](/en/how-to/use-faucet). * **Mainnet:** bridge USDT0 from another chain via LayerZero, or acquire through an exchange or custodian. **What changes when I port a contract from Ethereum?** Most contracts deploy unchanged. Three things to fix if they apply: * Don't mirror native balance in internal variables. `transferFrom` can drain native without calling the contract. * Don't transfer to `address(0)`. Both native and ERC-20 transfers to zero revert. * Don't rely on `EXTCODEHASH` for address-reuse detection. Permit-based approvals change native balance without a nonce increment. Full checklist: [USDT0 behavior on Stable](/en/explanation/usdt0-behavior). ### Resources **Where do I find tokenomics, roadmap, and architecture?** * [Tokenomics](/en/reference/tokenomics): STABLE supply, allocation, and vesting. * [Technical roadmap](/en/explanation/technical-roadmap): phased optimization plan. * [Tech overview](/en/explanation/tech-overview): StableBFT, Stable EVM, StableDB, and RPC design. * [USDT-specific features](/en/explanation/usdt-features-overview): detail on gas, blockspace, aggregation, and confidential transfer. **Where do I go for help?** * [Developer assistance](/en/reference/developer-assistance): FAQ and reference pointers. * [Discord](https://discord.gg/stablexyz): community support and protocol updates. * `bizdev@stable.xyz`: partnership and integration conversations. ### Next recommended * [**Quick start**](/en/tutorial/quick-start) — Send a first transaction on testnet. * [**Core concepts**](/en/explanation/core-concepts) — Learn the four core concepts you need before you build. * [**Learn overview**](/en/explanation/learn-overview) — Pick the docs path that fits what you're building. * [**Production readiness**](/en/how-to/production-readiness) — Validate an integration before shipping to mainnet. ## Gas pricing reference Transaction construction, gas estimation, and tooling configuration for Stable. :::note **Concept:** For why Stable uses a single-component fee model and how it compares to Ethereum, see [Gas pricing](/en/explanation/gas-pricing). ::: ### Transaction construction When constructing transactions on Stable, set `maxPriorityFeePerGas` to `0`. Clients should fetch the latest base fee from the most recent block and include a safety margin when computing `maxFeePerGas`. ```javascript // ethers.js v6 const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const maxPriorityFeePerGas = 0n; // always 0 on Stable const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // double the base fee as safety margin const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: parseEther("0.01"), maxFeePerGas, maxPriorityFeePerGas, }); ``` ```text Native USDT0 transfer confirmed. Fee ≈ 0.0000021 USDT0 at baseFee = 1 gwei. ``` ### Gas estimation Use `eth_estimateGas` and `eth_gasPrice` as you would on Ethereum. The key difference is that `eth_maxPriorityFeePerGas` will always return `0`. ```javascript const gasPrice = await provider.send("eth_gasPrice", []); const gasEstimate = await provider.estimateGas({ to: contractAddress, data: callData, }); const estimatedFeeInUSDT0 = gasPrice * gasEstimate; ``` ### Tooling configuration * **Hardhat / Foundry**: no special configuration needed; standard EVM settings work. If your config explicitly sets a priority fee, set it to `0`. * **Wallets**: hide or disable the priority tip input field. Displaying it may confuse users since the value has no effect. * **Monitoring**: fee analytics dashboards should not track priority fees. They will always be zero. ### Next recommended * [**Gas pricing concept**](/en/explanation/gas-pricing) — Understand why Stable uses a single-component fee model. * [**Ethereum comparison**](/en/explanation/ethereum-comparison) — Review every behavior difference you'll hit porting from Ethereum. * [**JSON-RPC API**](/en/reference/json-rpc-api) — Reference the `eth_*` methods Stable exposes. ## Gas waiver protocol This document specifies the Gas Waiver mechanism: transaction formats, marker routing, governance controls, and the Waiver Server API. :::note **Concept:** For what Gas Waiver is and why it exists, see [Gas waiver](/en/explanation/gas-waiver). For the how-to integration guide against the hosted Waiver Server, see [Enable gas-free transactions](/en/how-to/integrate-gas-waiver). ::: ### Abstract Gas Waiver enables gasless end-user transactions on Stable by allowing a small set of governance-approved addresses (“waivers”) to submit transactions with `gasPrice = 0`. Stable currently operates a waiver service (the “Waiver Server”) that you can integrate with to provide gasless UX without implementing protocol-specific wrapper logic. ### Scope This specification covers: * Protocol-level rules for gas-waived transactions * The wrapper transaction mechanism and marker address * Governance-controlled authorization and allowed targets * The Waiver Server interface for submitting signed user transactions ### Definitions * **Waiver**: An Ethereum address registered on-chain via validator governance that is authorized to submit gas-waived transactions. * **InnerTx**: The end user’s signed transaction with `gasPrice = 0`. * **WrapperTx**: A transaction signed by a waiver that transports the user’s `InnerTx` to the chain and authorizes execution. * **Marker address**: A sentinel address used to identify waiver wrapper transactions: `0x000000000000000000000000000000000000f333`. * **AllowedTarget**: A policy that limits a waiver to specific contract addresses and method selectors. ### Overview Gas Waiver uses a wrapper transaction pattern: 1. The user signs an `InnerTx` with `gasPrice = 0`. 2. A waiver wraps the `InnerTx` into a `WrapperTx` and broadcasts it. 3. Validators detect marker transactions, verify the waiver authorization and policy constraints, then execute the embedded `InnerTx`. Stable operates a waiver service (Waiver Server) that is registered on-chain as an authorized waiver. You integrate with the Waiver Server API to submit signed `InnerTx` payloads. ### Protocol specification #### Marker address routing A transaction is treated as a waiver wrapper transaction if and only if: * `to == 0x000000000000000000000000000000000000f333`. The protocol interprets the transaction `data` field as an encoded inner transaction payload and processes it using the waiver verification rules below. #### Authorization and policy checks For each candidate wrapper transaction, validators must enforce: 1. **Waiver authorization** * `WrapperTx.from` must be a waiver address registered on-chain via governance. 2. **Gas waiver** * `WrapperTx.gasPrice` must equal `0`. * `InnerTx.gasPrice` must equal `0`. 3. **Target allowlist** * `InnerTx.to` and the method selector extracted from `InnerTx.data` must be permitted by the waiver’s `AllowedTarget` policy. 4. **Value restrictions** * `WrapperTx.value` must equal `0`. If any check fails, validators reject the wrapper transaction and do not execute the inner transaction. #### Execution semantics If all checks pass: 1. The protocol executes `InnerTx` as the user, preserving the user’s `from`, `nonce`, and call semantics. 2. Gas accounting is handled by the waiver mechanism: the user pays no gas, and the waiver transaction uses `gasPrice = 0` by definition of the feature. 3. The wrapper transaction must supply sufficient `gasLimit` to cover the execution of `InnerTx` (including overhead for unwrap and verification). ### Transaction formats #### WrapperTx The wrapper transaction is signed by the waiver and sent to the marker address. ```javascript WrapperTx { from: waiver_address, to: 0x000000000000000000000000000000000000f333, value: 0, // must be zero data: RLP(InnerTx), // RLP-encoded inner transaction gasPrice: 0, // must be zero gasLimit: sufficient_for_inner, // must cover inner execution + overhead nonce: waiver_nonce } ``` #### InnerTx The inner transaction is signed by the end user. ```javascript InnerTx { from: user_address, to: target_contract, value: value, data: call_data, gasPrice: 0, // must be zero gasLimit: execution_gas, nonce: user_nonce } ``` ### Governance-controlled access Waiver authorization is governed on-chain by validator governance. Governance control provides: * Reviewable authorization of waiver addresses * On-chain transparency of waiver registration and updates * Revocation capability * Per-waiver scoping via `AllowedTarget` ### Security model #### End-user signature integrity The user signs the `InnerTx`. The waiver cannot modify the inner transaction payload without invalidating the signature. You must still ensure that the user signs only the intended transaction payload. #### Trust boundary Gas Waiver introduces a service dependency if partners route submissions through the Waiver Server: * Availability of the service affects the ability to submit gasless transactions. * Authorization remains on-chain; only registered waiver addresses can produce valid wrapper submissions. ### Integration You integrate by: 1. Collect a signed `InnerTx` from the user (`gasPrice = 0`). 2. Submit the signed inner transaction to the Waiver Server API. 3. Handle streamed results and surfacing transaction hashes to end users. ### Waiver server #### Overview The Waiver Server wraps and broadcasts signed user `InnerTx` payloads as waiver-authorized wrapper transactions. You do not need to construct wrapper transactions or operate a waiver address. #### Endpoints and base URLs Base URLs: * Mainnet: TBD * Testnet: `https://waiver.testnet.stable.xyz` #### Authentication All endpoints except health require bearer token authentication: ``` Authorization: Bearer ``` #### API ##### GET `/v1/health` Health check endpoint. Authentication: none. ##### POST `/v1/submit` Submit a batch of signed inner transactions. Authentication: required (`Bearer`). Request body: ```json { "transactions": ["0x", "0x"] } ``` Response is streamed as NDJSON (newline-delimited JSON). Each line corresponds to a submitted transaction index. Example: ```json {"index":0,"id":"abc123","success":true,"txHash":"0x..."} {"index":1,"id":"def456","success":false,"error":{"code":"VALIDATION_FAILED","message":"invalid signature"}} ``` ##### GET `/v1/submit` WebSocket interface for streaming submissions. Authentication: required (`Bearer`). #### Integration example ```javascript const WAIVER_SERVER = "https://waiver.testnet.stable.xyz"; async function submitGaslessTransaction(signedInnerTxHex, apiKey) { const response = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex], }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); console.log(result); } } } ``` #### Creating a user InnerTx You are responsible for constructing an `InnerTx` with `gasPrice = 0`, then collecting the user signature. Example: ```javascript import { ethers } from "ethers"; async function createInnerTx(userWallet, contractAddress, callData, nonce) { const innerTx = { to: contractAddress, data: callData, value: value, gasPrice: 0, // must be 0 for waiver gasLimit: 100000, nonce: nonce, chainId: 2201, // 988 for mainnet, 2201 for testnet }; return await userWallet.signTransaction(innerTx); } ``` #### Error codes * `PARSE_ERROR`: Failed to parse transaction * `INVALID_REQUEST`: Malformed request body * `BATCH_SIZE_EXCEEDED`: Batch size exceeds allowed maximum * `VALIDATION_FAILED`: Transaction validation failed * `BROADCAST_FAILED`: Failed to broadcast to chain * `RATE_LIMITED`: Rate limit exceeded * `QUEUE_FULL`: Server queue at capacity * `TIMEOUT`: Request timed out ### Next recommended * [**Zero gas transactions**](/en/how-to/zero-gas-transactions) — Demo-focused walkthrough with a receipt showing zero gas fee. * [**Enable gas-free transactions**](/en/how-to/integrate-gas-waiver) — Full hosted-API integration guide with batch submissions and error handling. * [**Self-hosted Gas Waiver**](/en/how-to/self-hosted-gas-waiver) — Run your own waiver infrastructure without the hosted API. ## Indexers Indexers and analytics platforms provide structured access to on-chain data, enabling developers to query transactions, balances, logs, events, and application-specific data at scale. Stable is EVM-compatible, so standard Ethereum indexing tools work seamlessly. This page lists current and upcoming indexing providers, along with the capabilities developers can expect. ### Overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :------------------------------------------------------------------- | :--------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------- | | [**Stablescan**](https://stablescan.xyz/) | Blockchain Explorer | [https://docs.etherscan.io/introduction](https://docs.etherscan.io/introduction) | Blockchain explorer; transaction, block, and contract visibility. | | [**The Graph**](https://thegraph.com/explorer/participants/indexers) | Indexer | [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) | Build, deploy, and query subgraphs using GraphQL. | | [**Goldsky**](https://goldsky.com/) | Indexer | [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) | High-performance indexing and real-time data streaming. | | [**Ormi Labs**](https://ormilabs.com/) | Indexer | [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) | Next-gen subgraph indexer with real-time data capabilities. | | [**Allium**](https://www.allium.so/) | Analytics / Data Platform | [https://docs.allium.so/](https://docs.allium.so/) | Normalized blockchain datasets & analytics tooling. | | [**CoinMarketCap**](https://coinmarketcap.com/api/) | Market Data Aggregator | [https://coinmarketcap.com/api/documentation/v1/](https://coinmarketcap.com/api/documentation/v1/) | Market prices, listings, and tracking tools. | | [**CoinGecko**](https://www.coingecko.com/en/api) | Market Data Aggregator | [https://docs.coingecko.com/](https://docs.coingecko.com/) | Independent market data with developer APIs. | | [**Dexscreener**](https://docs.dexscreener.com/) | DEX Analytics | [https://docs.dexscreener.com/](https://docs.dexscreener.com/) | Real-time DEX charts, liquidity analytics, and dashboards. | | [**DeBank**](https://debank.com/) | Portfolio / Wallet Analytics | [https://cloud.debank.com/](https://cloud.debank.com/) | EVM wallet tracking, transactions, and portfolio insights. | ### 1. Indexers Indexers transform raw blockchain data into searchable, queryable formats. They power dashboards, analytics, wallets, block explorers, and application backends. #### The Graph Decentralized indexing protocol powering data access for over 75,000 projects. **Capabilities** * Subgraphs for Stable * GraphQL-based querying * Distributed indexing network **Docs** Follow this quick-start guide to create, deploy, and query a subgraph within minutes: [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) #### Ormi Labs Ormi is a next-generation indexer built to deliver real-time and historical blockchain data at scale. **Capabilities** * Tip of the chain indexing * Sub-second query latency * No-code feature **Docs** Start querying real-time data with Ormi: [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) Learn how to query USDT0 data on Stable in real-time: [https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0](https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0) #### Goldsky High-performance indexing platform with instant subgraphs and developer tooling. **Capabilities** * Subgraph deployment and management * Webhook creation for real-time event streaming * Multi-subgraph sync to external databases **Docs** [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) For 24/7 support, contact [support@goldsky.com](mailto\:support@goldsky.com). ### 2. Analytics providers Analytics tools help teams track network activity, real-world payments, dashboards, usage flows, and contract interactions. #### Allium A foundational data platform for engineers and analysts across the blockchain ecosystem. **Capabilities** * Normalized blockchain datasets * Query tooling for analytics teams * Enterprise data infrastructure **Docs** Get started with Allium’s data platform and API resources: [https://www.allium.so/](https://www.allium.so/) **Get started**: Sign up at [allium.so](https://www.allium.so/) to get API access, then query normalized Stable on-chain datasets using the REST API. #### CoinMarketCap The largest global market tracking platform for crypto assets. **Capabilities** * Price tracking for Stable assets * Portfolio tool * Market data APIs **Docs** Explore the CMC API and integration resources: [https://coinmarketcap.com/api/](https://coinmarketcap.com/api/) **Get started**: Register for an API key at [coinmarketcap.com/api](https://coinmarketcap.com/api/) and query Stable asset price and market data via the REST API. #### CoinGecko The world’s largest independent crypto data aggregator. **Capabilities** * Market listings and price data * Historical analytics * API access for developers **Docs** Access CoinGecko’s API documentation: [https://www.coingecko.com/en/api](https://www.coingecko.com/en/api) **Get started**: Get a free or Pro API key from [coingecko.com/en/api](https://www.coingecko.com/en/api) and query Stable token price and market data via the REST API. #### Dexscreener Real-time charts and analytics for decentralized venues. **Capabilities** * Live DEX charts * Liquidity and pair analytics * Trading dashboards **Docs** Explore Dexscreener’s API endpoints and developer tools: [https://docs.dexscreener.com/](https://docs.dexscreener.com/) **Get started**: Browse the [Dexscreener API docs](https://docs.dexscreener.com/) to query real-time pair data, liquidity, and trading activity for Stable-based DEX pools. #### DeBank Portfolio tracker for Ethereum and EVM ecosystems. **Capabilities** * Wallet analytics * Transaction summaries * Portfolio tracking across chains **Docs** Read DeBank’s API reference and integration documentation: [https://docs.debank.com/](https://docs.debank.com/) **Get started**: Sign up for [DeBank Cloud](https://cloud.debank.com/) to access API keys and query Stable wallet balances, transaction history, and portfolio data. *** Have an indexing or analytics platform integrating Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## Settle invoices Each invoice maps to a unique, deterministic nonce derived from invoice metadata: invoice number, parties, amount, and due date. This nonce drives settlement via [ERC-3009](/en/explanation/erc-3009) and creates an immutable receipt that can be reconciled with existing accounting systems. ### How it works Both the buyer and the vendor independently compute the same nonce from the same invoice metadata. No external registry is required to coordinate payment. The nonce is derived deterministically: ``` nonce = keccak256(invoiceNumber, vendor, buyer, amount, dueDate) ``` When the buyer signs the ERC-3009 authorization using this nonce, the on-chain settlement event serves as a tamper-proof payment receipt. #### Settlement flow 1. **Invoice issued**: the vendor creates an invoice with a unique number, amount, and due date. 2. **Nonce computed**: both parties independently derive the same nonce from the invoice metadata. 3. **Buyer signs**: the buyer signs an ERC-3009 authorization off-chain using the deterministic nonce. The `validBefore` field can be set to the due date plus a grace period. 4. **Settlement**: the buyer or vendor submits `transferWithAuthorization` on-chain. Settlement confirms in under a second. 5. **Reconciliation**: the emitted `AuthorizationUsed` event contains the nonce, linking the on-chain settlement to the exact invoice. The `Transfer` event in the same transaction verifies sender, recipient, and amount. #### Double-payment prevention The nonce is consumed on-chain upon payment. The same invoice cannot be settled twice; resubmitting an authorization with an already-used nonce reverts. ### What makes it different Traditional B2B invoicing involves bank wires (1–5 business days), manual reconciliation, and no cryptographic proof of payment tied to the invoice itself. With deterministic nonces, the on-chain payment is self-documenting: the nonce links the settlement to the exact invoice, and the blockchain event log provides an immutable audit trail. | **Aspect** | **Traditional (bank wire)** | **Stable (ERC-3009)** | | :------------- | :-------------------------------------- | :-------------------------------------------------------- | | Settlement | 1–5 business days | Under 1 second | | Reconciliation | Manual matching against bank statements | `AuthorizationUsed` event links payment to invoice nonce | | Payment proof | Bank confirmation letter | On-chain transaction, cryptographically linked to invoice | | Intermediaries | Correspondent banks | None | | Fees | Wire fees ($15–45) + FX spread | \~0.00021 USDT0 (or 0 with Gas Waiver) | **See also:** * [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009) * [Gas Waiver](/en/how-to/integrate-gas-waiver) ## JSON-RPC API ### eth\_namespace | API | support | | ------------------------------------------- | ------- | | eth\_syncing | ✅ | | eth\_gasPrice | ✅ | | eth\_maxPriorityFeePerGas | ✅ | | eth\_feeHistory | ✅ | | eth\_blobBaseFee | ❌ | | eth\_chainId | ✅ | | eth\_blockNumber | ✅ | | eth\_getBalance | ✅ | | eth\_getProof | ✅ | | eth\_getHeaderByNumber | ❌ | | eth\_getHeaderByHash | ❌ | | eth\_getBlockByNumber | ✅ | | eth\_getBlockByHash | ✅ | | eth\_getUncleByBlockNumberAndIndex | ❌ | | eth\_getUncleByBlockHashAndIndex | ❌ | | eth\_getUncleCountByBlockNumber | ❌ | | eth\_getUncleCountByBlockHash | ❌ | | eth\_getCode | ✅ | | eth\_getStorageAt | ✅ | | eth\_getBlockReceipts | ❌ | | eth\_call | ✅ | | eth\_simulateV1 | ❌ | | eth\_estimateGas | ✅ | | eth\_createAccessList | ❌ | | eth\_getBlockTransactionCountByNumber | ✅ | | eth\_getBlockTransactionCountByHash | ✅ | | eth\_getTransactionByBlockNumberAndIndex | ✅ | | eth\_getTransactionByBlockHashAndIndex | ✅ | | eth\_getRawTransactionByBlockNumberAndIndex | ❌ | | eth\_getRawTransactionByBlockHashAndIndex | ❌ | | eth\_getTransactionCount | ✅ | | eth\_getTransactionByHash | ✅ | | eth\_getRawTransactionByHash | ❌ | | eth\_getTransactionReceipt | ✅ | | eth\_sendTransaction | ✅ | | eth\_fillTransaction | ❌ | | eth\_sendRawTransaction | ✅ | | eth\_sign | ✅ | | eth\_signTransaction | ❌ | | eth\_pendingTransactions | ✅ | | eth\_resend | ✅ | | eth\_accounts | ✅ | | eth\_subscribe | ✅ | | eth\_unsubscribe | ✅ | | eth\_getTransactionLogs | ✅ | | eth\_signTypedData | ✅ | | eth\_newPendingTransactionFilter | ✅ | | eth\_newBlockFilter | ✅ | | eth\_newFilter | ✅ | | eth\_getFilterChanges | ✅ | | eth\_getFilterLogs | ✅ | | eth\_uninstallFilter | ✅ | | eth\_getLogs | ✅ | ### debug\_namespace | API | support | | ---------------------------------- | ------- | | debug\_accountRange | ❌ | | debug\_backtraceAt | ❌ | | debug\_blockProfile | ✅ | | debug\_chaindbCompact | ❌ | | debug\_chaindbProperty | ❌ | | debug\_cpuProfile | ✅ | | debug\_dbAncient | ❌ | | debug\_dbAncients | ❌ | | debug\_dbGet | ❌ | | debug\_dumpBlock | ❌ | | debug\_freeOSMemory | ✅ | | debug\_freezeClient | ❌ | | debug\_gcStats | ✅ | | debug\_getAccessibleState | ❌ | | debug\_getBadBlocks | ❌ | | debug\_getRawBlock | ❌ | | debug\_getRawHeader | ❌ | | debug\_getRawTransaction | ❌ | | debug\_getModifiedAccountsByHash | ❌ | | debug\_getModifiedAccountsByNumber | ❌ | | debug\_getRawReceipts | ❌ | | debug\_goTrace | ✅ | | debug\_intermediateRoots | ✅ | | debug\_memStats | ✅ | | debug\_mutexProfile | ✅ | | debug\_preimage | ❌ | | debug\_printBlock | ✅ | | debug\_setBlockProfileRate | ✅ | | debug\_setGCPercent | ✅ | | debug\_setHead | ❌ | | debug\_setMutexProfileFraction | ✅ | | debug\_setTrieFlushInterval | ❌ | | debug\_stacks | ✅ | | debug\_standardTraceBlockToFile | ❌ | | debug\_standardTraceBadBlockToFile | ❌ | | debug\_startCPUProfile | ✅ | | debug\_startGoTrace | ✅ | | debug\_stopCPUProfile | ✅ | | debug\_stopGoTrace | ✅ | | debug\_storageRangeAt | ❌ | | debug\_traceBadBlock | ❌ | | debug\_traceBlock | ❌ | | debug\_traceBlockByNumber | ✅ | | debug\_traceBlockByHash | ✅ | | debug\_traceBlockFromFile | ❌ | | debug\_traceCall | ❌ | | debug\_traceChain | ❌ | | debug\_traceTransaction | ✅ | | debug\_verbosity | ❌ | | debug\_vmodule | ❌ | | debug\_writeBlockProfile | ✅ | | debug\_writeMemProfile | ✅ | | debug\_writeMutexProfile | ✅ | ### Next recommended * [**Gas pricing reference**](/en/reference/gas-pricing-api) — Construct EIP-1559 transactions against Stable's base-fee-only model. * [**EIP-7702 reference**](/en/reference/eip-7702-api) — Build type-4 transactions with the `authorizationList` field. * [**System modules reference**](/en/reference/system-modules-api-overview) — Call Bank, Distribution, and Staking at their fixed precompile addresses. ## Mainnet information Everything you need to know to access Stable Mainnet. ### Network overview | Configuration | Value | | ---------------- | -------------- | | **Network Name** | Stable Mainnet | | **Chain ID** | `988` | | **Gas Token** | USDT0 | | **Gov Token** | STABLE | | **Block Time** | \~0.7 seconds | ### Block explorers | Explorer | URL | | -------------- | ------------------------------------------------ | | **Stablescan** | [https://stablescan.xyz](https://stablescan.xyz) | ### RPC endpoints #### Primary endpoints | Type | Endpoint | Purpose | | ---------------- | ------------------------------------------------ | ----------------- | | **EVM JSON-RPC** | [https://rpc.stable.xyz](https://rpc.stable.xyz) | EVM transactions | | **WebSocket** | wss\://rpc.stable.xyz | Real-time updates | :::note The public RPC endpoint is rate-limited to **1,000 requests per 10 seconds per IP**. Requests over the limit return `HTTP 429`. For higher throughput, use a [third-party RPC provider](/en/reference/rpc-providers). ::: ### Chain information | Parameter | EVM | | ------------- | ------- | | **Chain ID** | `988` | | **Gas Token** | `USDT0` | | **Decimals** | 18 | ### Tools | Tool | URL | Description | | ------------- | --------------------------------------------------------- | --------------- | | **Snapshots** | See [Node Operators Guide](/en/how-to/use-node-snapshots) | Chain snapshots | ## Version history Complete version history and related documentation for the Stable Mainnet. ### Current version information * **Current Version**: `v1.3.1` * **Next Upgrade**: `TBD` * **Upgrade Height**: `TBD` * **Expected Time**: `TBD` ### Version history #### Current & previous versions | Version | Commit | Upgrade Height | Binary | Status | | ---------- | --------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | **v1.3.1** | `f85d155` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.1-linux-arm64-mainnet.tar.gz) | Current | | **v1.3.0** | `dd103ec` | 24,077,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.0-linux-arm64-mainnet.tar.gz) | | | **v1.2.2** | `76da1da` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-arm64-mainnet.tar.gz) | | | **v1.2.1** | `7955bb7` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-arm64-mainnet.tar.gz) | | | **v1.2.0** | `47e355b` | 12,004,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-arm64-mainnet.tar.gz) | | | **v1.1.4** | `c795773` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-arm64-mainnet.tar.gz) | | | **v1.1.2** | `3d83aa3` | 3,263,600 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-arm64-mainnet.tar.gz) | | | **v1.1.0** | `17ceaa7` | 1,694,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-arm64-mainnet.tar.gz) | | | **v1.0.0** | `d996084` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-arm64-mainnet.tar.gz) | Genesis | ### Related documentation * [Upgrade Guide](/en/how-to/upgrade-node) - Step-by-step upgrade procedures * [Mainnet Information](/en/reference/mainnet-information) - Current network details ## Network routing Network routing providers optimizing connectivity and data delivery for applications on Stable. ### Overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :---------------------------------- | :----------------------- | :---------------------------------- | :--------------------------------- | | [**Optimum**](https://optimum.xyz/) | Decentralized networking | [optimum.xyz](https://optimum.xyz/) | High-performance routing for dApps | ### Optimum A decentralized internet protocol optimized for speed and scalable web3 interactions. **Capabilities** * High-performance decentralized networking * Faster application data routing * Reliable infra for dApps **Get started**: Visit [optimum.xyz](https://optimum.xyz/) to learn how to route your Stable dApp traffic through Optimum's decentralized network infrastructure. *** Have a networking integration with Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## Network upgrades This guide covers all configuration options for Stable nodes, including optimization for different use cases. ### Configuration files overview Stable nodes use two main configuration files: * **`config.toml`**: Core StableBFT configuration * **`app.toml`**: Application-specific configuration Both files are located in `~/.stabled/config/` ### Core configuration (config.toml) #### Basic settings :::code-group ```toml [Mainnet] # The ID of the chain to join chain_id = "stable_988-1" # A custom human-readable name for this node moniker = "your-node-name" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb db_backend = "goleveldb" ``` ```toml [Testnet] # The ID of the chain to join chain_id = "stabletestnet_2201-1" # A custom human-readable name for this node moniker = "your-node-name" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb db_backend = "goleveldb" ``` ::: #### P2P configuration :::code-group ```toml [Mainnet] [p2p] # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial external_address = "YOUR_PUBLIC_IP:26656" # Comma separated list of seed nodes seeds = "17a539fda42863a99755547e1c9b3ec4c38a4439@seed1.stable.xyz:26656" # Comma separated list of persistent peers persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656" ``` ```toml [Testnet] [p2p] # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial external_address = "YOUR_PUBLIC_IP:26656" # Comma separated list of seed nodes seeds = "39e061b167162f6621ddadcf1be21d6fa585a468@seed1.testnet.stable.xyz:26656" # Comma separated list of persistent peers persistent_peers = "5ed0f977a26ccf290e184e364fb04e268ef16430@peer1.testnet.stable.xyz:26656" ``` ::: Additional P2P settings (same for both networks): ```toml # Maximum number of inbound peers max_num_inbound_peers = 50 # Maximum number of outbound peers max_num_outbound_peers = 30 # Toggle to disable guard against peers connecting from the same ip allow_duplicate_ip = false # Peer connection configuration handshake_timeout = "20s" dial_timeout = "3s" # Time to wait before flushing messages out on the connection flush_throttle_timeout = "100ms" # Maximum size of a message packet payload max_packet_msg_payload_size = 1024 # Rate limiting send_rate = 5120000 # 5 MB/s recv_rate = 5120000 # 5 MB/s # Seed mode (for seed nodes only) seed_mode = false # Enable peer exchange reactor pex = true ``` #### RPC server configuration ```toml [rpc] # TCP or UNIX socket address for the RPC server laddr = "tcp://127.0.0.1:26657" # A list of origins a cross-domain request can be executed from cors_allowed_origins = ["*"] # A list of methods the client is allowed to use with cross-domain requests cors_allowed_methods = ["HEAD", "GET", "POST"] # A list of non simple headers the client is allowed to use with cross-domain requests cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] # TCP or UNIX socket address for the gRPC server grpc_laddr = "tcp://127.0.0.1:9090" # Maximum number of simultaneous connections grpc_max_open_connections = 900 # Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool unsafe = false # Maximum number of simultaneous connections (including WebSocket) max_open_connections = 900 # Maximum number of unique clientIDs that can connect max_subscription_clients = 100 # Maximum number of unique queries a given client can subscribe to max_subscriptions_per_client = 5 # How long to wait for a tx to be committed timeout_broadcast_tx_commit = "10s" # Maximum size of request body max_body_bytes = 1000000 # Maximum size of request header max_header_bytes = 1048576 ``` #### Mempool configuration ```toml [mempool] # Mempool version to use version = "v1" # Recheck enabled recheck = true # Broadcast enabled broadcast = true # Maximum number of transactions in the mempool size = 3000 # Limit the total size of all txs in the mempool max_txs_bytes = 1073741824 # 1GB # Size of the cache cache_size = 10000 # Do not remove invalid transactions from the cache keep-invalid-txs-in-cache = false # Maximum size of a single transaction max_tx_bytes = 1048576 # 1MB # Maximum size of a batch of transactions to send to a peer max_batch_bytes = 0 ``` #### Consensus configuration ```toml [consensus] # How long we wait for a proposal block before prevoting nil timeout_propose = "5s" # How much timeout_propose increases with each round timeout_propose_delta = "10ms" # How long we wait after receiving +2/3 prevotes timeout_prevote = "150ms" # How much the timeout_prevote increases with each round timeout_prevote_delta = "10ms" # How long we wait after receiving +2/3 precommits timeout_precommit = "150s" # How much the timeout_precommit increases with each round timeout_precommit_delta = "10ms" # Make progress as soon as we have all the precommits skip_timeout_commit = false # Enable/disable double sign check double_sign_check_height = 2 # EmptyBlocks mode create_empty_blocks = true create_empty_blocks_interval = "0s" # Reactor sleep duration peer_gossip_sleep_duration = "100ms" peer_query_maj23_sleep_duration = "2s" ``` ### Application configuration (app.toml) #### Basic application settings ```toml # Pruning strategy pruning = "default" # HaltHeight contains a non-zero block height at which a node will halt halt-height = 0 # HaltTime contains a non-zero time at which a node will halt halt-time = 0 # MinRetainBlocks defines the number of blocks for which a node will retain min-retain-blocks = 0 # InterBlockCache enables inter-block caching inter-block-cache = true # IndexEvents defines the set of events in the form {eventType}.{attributeKey} index-events = [] # IavlCacheSize set the size of the iavl tree cache iavl-cache-size = 781250 ``` #### API configuration ```toml [api] # Enable defines if the API server should be enabled enable = true # Swagger defines if swagger documentation should automatically be registered swagger = true # Address defines the API server to listen on address = "tcp://0.0.0.0:1317" # MaxOpenConnections defines the number of maximum open connections max-open-connections = 1000 # EnabledUnsafeCORS defines if CORS should be enabled enabled-unsafe-cors = true ``` #### gRPC configuration ```toml [grpc] # Enable defines if the gRPC server should be enabled enable = true # Address defines the gRPC server address to bind to address = "0.0.0.0:9090" ``` #### EVM JSON-RPC configuration ```toml [json-rpc] # Enable the JSON-RPC server enable = true # Address to bind the JSON-RPC server address = "0.0.0.0:8545" # Address to bind the WebSocket server ws-address = "0.0.0.0:8546" # APIs to enable api = "eth,net,web3,txpool,personal,debug" # Gas cap for eth_call/estimateGas gas-cap = 25000000 # EVM timeout for eth_call/estimateGas evm-timeout = "5s" # Tx fee cap for transactions txfee-cap = 1 # Filter cap for eth_getLogs filter-cap = 200 # FeeHistory cap feehistory-cap = 100 # Block range cap for eth_getLogs logs-cap = 10000 # Block range cap block-range-cap = 10000 # HTTP timeout http-timeout = "30s" # HTTP idle timeout http-idle-timeout = "120s" # Allow unprotected transactions allow-unprotected-txs = true # Maximum number of transactions in the pool max-tx-in-pool = 3000 # Enable indexer enable-indexer = false # Enable metrics metrics = true ``` ### Configuration profiles #### Full node (default) Balanced configuration for full nodes: ```bash # config.toml adjustments sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 50/' ~/.stabled/config/config.toml sed -i 's/^max_num_outbound_peers = .*/max_num_outbound_peers = 30/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^pruning = ".*"/pruning = "default"/' ~/.stabled/config/app.toml sed -i 's/^snapshot-interval = .*/snapshot-interval = 1000/' ~/.stabled/config/app.toml ``` #### Archive node No pruning, full history: ```bash # config.toml adjustments sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^pruning = ".*"/pruning = "nothing"/' ~/.stabled/config/app.toml ``` #### RPC node Public RPC endpoint configuration: ```bash # config.toml adjustments sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 30/' ~/.stabled/config/config.toml sed -i 's/^max_open_connections = .*/max_open_connections = 30/' ~/.stabled/config/config.toml sed -i 's/^cors_allowed_origins = .*/cors_allowed_origins = ["*"]/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^enable = .*/enable = true/' ~/.stabled/config/app.toml sed -i 's/^swagger = .*/swagger = true/' ~/.stabled/config/app.toml sed -i 's/^enabled-unsafe-cors = .*/enabled-unsafe-cors = true/' ~/.stabled/config/app.toml ``` ### Monitoring configuration #### Prometheus metrics ```toml # config.toml [instrumentation] # Enable Prometheus metrics prometheus = true # Metrics listen address prometheus_listen_addr = ":26660" # Namespace for metrics namespace = "stablebft" ``` #### Logging ```toml # config.toml [log] # Log level (trace|debug|info|warn|error|fatal|panic) level = "info" # Log format (plain|json) format = "plain" ``` ### Applying configuration changes After making configuration changes: ```bash # Restart the node sudo systemctl restart ${SERVICE_NAME} # Check logs for errors sudo journalctl -u ${SERVICE_NAME} -f # Verify configuration loaded curl localhost:26657/status | jq '.result.node_info' ``` ### Next steps * [Set up Monitoring](/en/how-to/monitor-node) for your node * Review [Troubleshooting Guide](/en/how-to/troubleshoot-node) for common issues ## Operate Operate covers running a Stable node: full or archive, testnet or mainnet, from install through monitoring. For the chain-level behavior your node enforces (fee model, finality, USDT0 as gas), see [Gas pricing](/en/explanation/gas-pricing), [Finality](/en/explanation/finality), and the [Architecture overview](/en/explanation/core-optimization-overview). ### Quick links * **[System Requirements](/en/reference/node-system-requirements)** - Hardware and software requirements for different node types * **[Installation Guide](/en/how-to/install-node)** - Step-by-step installation instructions for various platforms * **[Configuration](/en/reference/node-configuration)** - Detailed configuration options and best practices * **[Snapshots & Sync](/en/how-to/use-node-snapshots)** - Fast sync options using snapshots * **[Create a validator](/en/how-to/run-validator)** - Register a synced node as a validator and self-delegate * **[Upgrade Guide](/en/how-to/upgrade-node)** - Node upgrade procedures and version history * **[Monitoring](/en/how-to/monitor-node)** - Tools and metrics for node monitoring * **[Troubleshooting](/en/how-to/troubleshoot-node)** - Common issues and solutions To read validator data (stake, uptime, voting history) from the chain instead of running `stabled`, see [Index validator data](/en/how-to/index-validator-data). ### Node types #### Full node A full node maintains a complete copy of the blockchain and validates all transactions and blocks. Full nodes: * Verify all transactions and blocks * Maintain the entire blockchain history * Can serve data to other nodes * Support the network's decentralization #### Archive node An archive node stores the complete history of all states and can serve historical queries. Archive nodes: * Store all historical states * Support historical queries at any block height * Require significantly more storage * Essential for block explorers and analytics ### Network information For complete network details including RPC endpoints, block explorers, and chain parameters, see: * **[Mainnet](/en/reference/mainnet-information)** - Mainnet details * **[Testnet](/en/reference/testnet-information)** - Testnet details ### Support and community * **Discord**: [Join the Stable Discord](https://discord.gg/stablexyz) ### Quick start For experienced operators who want to get started quickly: 1. Check [System Requirements](/en/reference/node-system-requirements) 2. Follow the [Installation Guide](/en/how-to/install-node) 3. Configure your node using [Configuration Guide](/en/reference/node-configuration) 4. Speed up sync with [Snapshots](/en/how-to/use-node-snapshots) 5. Monitor your node with [Monitoring Guide](/en/how-to/monitor-node) For network parameters and RPC endpoints, see [Mainnet Information](/en/reference/mainnet-information) or [Testnet Information](/en/reference/testnet-information). ### How node ops connect to the chain Running a node means enforcing Stable's chain-level rules. These pages explain the behavior your node implements: * **[Contracts overview](/en/explanation/contracts-overview)** covers the fee model, JSON-RPC surface, and system modules your node serves. * **[Finality](/en/explanation/finality)** explains single-slot finality and what "confirmed" means at the consensus layer. * **[Architecture overview](/en/explanation/core-optimization-overview)** walks through consensus, execution, database, and RPC layers. * **[Gas pricing](/en/explanation/gas-pricing)** explains how USDT0-denominated fees are priced and collected. This page outlines the hardware and software requirements for running different types of Stable nodes. ### Hardware requirements #### Full node (minimum) | Component | Requirement | Notes | | ----------- | ----------------------------- | ---------------------------------------- | | **CPU** | 4 cores | AMD Ryzen 5 / Intel Core i5 or better | | **RAM** | 8 GB | 16 GB recommended for better performance | | **Storage** | 500 GB NVMe/SSD | Write throughput > 1000 MiBps required | | **Network** | 100 Mbps | Stable, low-latency connection | | **OS** | Ubuntu 22.04/24.04, Debian 12 | 64-bit Linux required | #### Full node (recommended) | Component | Requirement | Notes | | ----------- | ------------ | ------------------------------------- | | **CPU** | 8 cores | AMD Ryzen 7 / Intel Core i7 or better | | **RAM** | 16 GB | 32 GB for optimal performance | | **Storage** | 1 TB NVMe | Write throughput > 2000 MiBps | | **Network** | 1 Gbps | Dedicated connection preferred | | **OS** | Ubuntu 24.04 | Latest LTS recommended | #### Archive node | Component | Requirement | Notes | | ----------- | ------------ | ----------------------------------------- | | **CPU** | 16 cores | AMD Ryzen 9 / Intel Core i9 or equivalent | | **RAM** | 32 GB | 64 GB recommended | | **Storage** | 4 TB NVMe | Fast growing, plan for expansion | | **Network** | 1 Gbps | Unmetered connection required | | **OS** | Ubuntu 24.04 | Latest LTS recommended | ### Software requirements #### Operating system ##### Supported distributions * **Ubuntu 24.04 LTS** (recommended) * **Ubuntu 22.04 LTS** * **Debian 12 (Bookworm)** ##### System dependencies ```bash # Update system packages sudo apt update && sudo apt upgrade -y # Install essential tools sudo apt install -y \ build-essential \ git \ wget \ curl \ jq \ lz4 \ zstd \ htop \ net-tools \ ufw ``` ### Network requirements #### Bandwidth usage | Node Type | Download | Upload | Monthly Data | | ------------ | -------------- | ------------- | ------------ | | Full Node | \~50 Mbps avg | \~25 Mbps avg | \~15 TB | | Archive Node | \~100 Mbps avg | \~50 Mbps avg | \~30 TB | ### Cloud provider recommendations #### AWS * **Full node**: t3.xlarge or c5.xlarge * **Archive node**: m5.2xlarge or c5.2xlarge * **Storage**: gp3 with provisioned IOPS #### Google Cloud * **Full node**: n2-standard-4 * **Archive node**: n2-standard-8 * **Storage**: pd-ssd or pd-extreme #### Azure * **Full node**: Standard\_D4s\_v5 * **Archive node**: Standard\_D8s\_v5 * **Storage**: Premium SSD v2 #### DigitalOcean * **Full node**: General Purpose 8GB * **Archive node**: CPU-Optimized 16GB * **Storage**: Volume Block Storage ### Monitoring requirements For production deployments, ensure you have: * **Prometheus**: For metrics collection * **Grafana**: For visualization * **AlertManager**: For alerting * **Node exporter**: For system metrics * **Log aggregation**: ELK or Loki recommended ### Security considerations #### System hardening * Keep OS and packages updated * Configure automatic security updates * Use SSH keys only (disable password auth) * Configure fail2ban * Enable firewall (UFW/iptables) * Regular security audits ### Pre-installation checklist Before proceeding with installation, verify: * [ ] Hardware meets minimum requirements * [ ] Operating system is supported and updated * [ ] Storage has sufficient IOPS * [ ] Network bandwidth is adequate * [ ] Firewall rules are configured * [ ] System monitoring is set up * [ ] Backup strategy is defined * [ ] Security measures are in place ## Oracles Oracles provide smart contracts with off-chain data such as asset prices. RedStone operates price feeds on Stable. ### Overview table | **Provider** | **Category** | **Supported Pairs** | **Docs / Get Started** | **Notes** | | :-------------------------------------------- | :----------------- | :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | :-------------- | | [**RedStone**](https://www.redstone.finance/) | Oracle Price Feeds | BTC, ETH, USDT, USDC, PYUSD, XAUt, frxUSD, FXS, LBTC, sfrxETH/ETH, sfrxUSD, SolvBTC, sthUSD, thBILL, weETH | [https://docs.redstone.finance/docs/dapps/redstone-push/](https://docs.redstone.finance/docs/dapps/redstone-push/) | Live on mainnet | ### RedStone RedStone provides oracle price feeds on Stable through its [Push model](https://docs.redstone.finance/docs/dapps/redstone-push/). Feed contracts expose the Chainlink-compatible `AggregatorV3Interface`. **Capabilities** * Push-based price feeds with configurable deviation thresholds and heartbeat intervals * Chainlink-compatible `AggregatorV3Interface` with `latestRoundData()`, `decimals()`, and `description()` * Coverage for blue-chip assets, stablecoins, LSTs, LRTs, and yield-bearing / fundamental-priced assets #### Mainnet price feed addresses Source: [RedStone push feeds dashboard](https://app.redstone.finance/push-feeds?networks=stable\&testnets=true) | **Price Feed** | **Contract Address** | **Deviation** | **Heartbeat** | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------- | :------------ | :------------ | | **BTC / USD** | [0x687103bA8CC2f66C94696182Ef410400Da45fb24](https://stablescan.xyz/address/0x687103bA8CC2f66C94696182Ef410400Da45fb24) | 0.5% | 6h | | **ETH / USD** | [0x457BE3C697c644bF329C2C3ea79EbF1D254d603a](https://stablescan.xyz/address/0x457BE3C697c644bF329C2C3ea79EbF1D254d603a) | 0.5% | 6h | | **USDT / USD** | [0x58264801fadCd8598D3EE993572ADe9cA27F42c8](https://stablescan.xyz/address/0x58264801fadCd8598D3EE993572ADe9cA27F42c8) | 0.5% | 6h | | **USDC / USD** | [0x8ea3C667C264BbdaA1dA7638904b8671F451c7F9](https://stablescan.xyz/address/0x8ea3C667C264BbdaA1dA7638904b8671F451c7F9) | 0.5% | 6h | | **PYUSD / USD** | [0x1c30dA143E97c228102A5cAe3960dBBB41321604](https://stablescan.xyz/address/0x1c30dA143E97c228102A5cAe3960dBBB41321604) | 0.5% | 6h | | **XAUt / USD** | [0xd5E244accc514b56DCAD89897DD44499E7C35a05](https://stablescan.xyz/address/0xd5E244accc514b56DCAD89897DD44499E7C35a05) | 0.5% | 6h | | **frxUSD / USD** | [0xB5197ca89507FE045e8ce9996593D35071915EB7](https://stablescan.xyz/address/0xB5197ca89507FE045e8ce9996593D35071915EB7) | 0.5% | 6h | | **FXS / USD** | [0xC3b182aee94AECeCa39b072942f3Ce4B87465517](https://stablescan.xyz/address/0xC3b182aee94AECeCa39b072942f3Ce4B87465517) | 0.5% | 6h | | **LBTC / USD** | [0x80295Cf12E28f3F943304BFd6C2A2C044e731aaB](https://stablescan.xyz/address/0x80295Cf12E28f3F943304BFd6C2A2C044e731aaB) | 0.5% | 6h | | **sfrxETH / ETH** | [0x29533E113D803ab1967F6CB9495B95DC8C1EA594](https://stablescan.xyz/address/0x29533E113D803ab1967F6CB9495B95DC8C1EA594) | 0.5% | 6h | | **sfrxUSD / FUNDAMENTAL** | [0x71784611831b9566df7301A78bC1B3d29a8737bF](https://stablescan.xyz/address/0x71784611831b9566df7301A78bC1B3d29a8737bF) | 0.5% | 6h | | **SolvBTC / FUNDAMENTAL** | [0x58fa68A373956285dDfb340EDf755246f8DfCA16](https://stablescan.xyz/address/0x58fa68A373956285dDfb340EDf755246f8DfCA16) | 0.01% | 24h | | **sthUSD / FUNDAMENTAL** | [0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db](https://stablescan.xyz/address/0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db) | 0.5% | 12h | | **thBILL / FUNDAMENTAL / USD** | [0x7532df197a36587aeD2B9A59785c8BeD182FA62D](https://stablescan.xyz/address/0x7532df197a36587aeD2B9A59785c8BeD182FA62D) | 0.5% | 6h | | **weETH / FUNDAMENTAL** | [0xD57b79401956BE4872D3d03F0C920639335e350F](https://stablescan.xyz/address/0xD57b79401956BE4872D3d03F0C920639335e350F) | 0.5% | 6h | ### Reading a price feed Feed contracts implement the Chainlink-compatible [`AggregatorV3Interface`](https://docs.redstone.finance/docs/dapps/redstone-push/). The same consumer pattern used for any Chainlink-style price feed applies. ```solidity pragma solidity ^0.8.25; interface AggregatorV3Interface { function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ); function decimals() external view returns (uint8); function description() external view returns (string memory); } contract OracleConsumer { AggregatorV3Interface public oracle; constructor(address oracleAddress) { oracle = AggregatorV3Interface(oracleAddress); } function getLatestPriceData() external view returns ( uint80 roundId, int256 answer, uint256 updatedAt ) { (roundId, answer, , updatedAt, ) = oracle.latestRoundData(); return (roundId, answer, updatedAt); } } ``` :::note RedStone push feeds on Stable are currently available on mainnet only. Always verify on-chain `updatedAt` against your application's freshness tolerance before consuming a price. ::: #### Reading a feed directly You can read any RedStone feed without deploying a consumer contract. The following call reads the ETH/USD feed: ```bash cast call 0x457BE3C697c644bF329C2C3ea79EbF1D254d603a "latestRoundData()(uint80,int256,uint256,uint256,uint80)" --rpc-url https://rpc.stable.xyz ``` #### Deploying a consumer to Stable mainnet This assumes you have Foundry installed and a funded wallet. See the [Deploy Smart Contract](/en/tutorial/smart-contract) tutorial for full setup instructions. 1. Save the contract above to `src/OracleConsumer.sol` in a Foundry project. 2. Deploy with the BTC/USD mainnet feed address: ```bash source .env ; forge create src/OracleConsumer.sol:OracleConsumer --broadcast --rpc-url $STABLE_MAINNET_RPC_URL --private-key $PRIVATE_KEY --constructor-args 0x687103bA8CC2f66C94696182Ef410400Da45fb24 ``` 3. Read the latest price from your deployed contract: ```bash cast call "getLatestPriceData()(uint80,int256,uint256)" --rpc-url $STABLE_MAINNET_RPC_URL ``` ### Have an oracle integrating Stable? Reach the team at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) to be listed on this page. ## Send and receive USDT0 On Stable, P2P payments settle in under a second. Two transfer methods are available depending on the use case. ### Native transfer The sender signs and broadcasts a transaction directly to the recipient. This costs 21,000 gas (\~0.00021 USDT0 at 100 gwei). No contract interaction is required. A native transfer is the simplest path: the sender signs a transaction sending USDT0 directly to the recipient. This is equivalent to entering an amount and selecting "Send" in any payment app. For a code walkthrough, see [Send your First USDT0](/en/tutorial/send-usdt0). ### Application-initiated transfer (ERC-3009) The sender signs an off-chain authorization. An application or facilitator submits the transaction on their behalf. Combined with the [Gas Waiver](/en/how-to/integrate-gas-waiver), the gas cost is 0. [ERC-3009](/en/explanation/erc-3009) is more appropriate for application-initiated payments (e.g., a payment in a web app) because it separates the signer from the submitter. The sender only signs an authorization off-chain, and the application or a facilitator handles on-chain submission. ### What makes it different On traditional payment rails, a P2P transfer involves bank processing, clearing, and settlement that can take 1–3 business days. Even on other blockchains, the sender needs to hold a volatile gas token (ETH, SOL) alongside the payment token. On Stable, the sender holds only USDT0, gas can be waived, and settlement is final in under a second. | **Aspect** | **Traditional (bank transfer)** | **Other blockchains** | **Stable** | | :--------------- | :------------------------------ | :-------------------------------------------------- | :--------------------------------------------------- | | Settlement time | 1–3 business days | Seconds to minutes, depending on the chain | Under 1 second (single-slot finality) | | Required assets | Fiat currency | Payment token + separate gas token (ETH, SOL, etc.) | USDT0 only (single asset) | | Transaction cost | Wire / intermediary fees | Can spike with network congestion | \~0.00021 USDT0 native transfer or $0 via Gas Waiver | **See also:** * [Send your First USDT0](/en/tutorial/send-usdt0) * [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009) * [Gas Waiver](/en/how-to/integrate-gas-waiver) ## Bill per API request Monetize any HTTP endpoint with per-request pricing using [x402](/en/explanation/x402) middleware. A server declares its price, a client pays per call, and settlement happens within the request lifecycle. No accounts, no API keys, no billing cycles. ### How it works The server adds x402 middleware to the endpoints it wants to monetize. When a request arrives without payment, the server responds with HTTP `402 Payment Required` and a `PAYMENT-REQUIRED` header containing the price, token, and network. The client signs an [ERC-3009](/en/explanation/erc-3009) authorization for the specified amount and resubmits. The facilitator settles the payment on-chain, and the server returns the resource. #### Request flow 1. Client sends an HTTP request to the server. 2. Server returns `402 Payment Required` with a `PAYMENT-REQUIRED` header containing the price, token, network, and recipient. 3. Client signs an ERC-3009 authorization for the specified amount and resubmits the request with a `PAYMENT-SIGNATURE` header. 4. Facilitator verifies the signature and settles the transfer on-chain. 5. Server returns the resource with a `PAYMENT-RESPONSE` header containing the settlement receipt. #### Pricing Prices are denominated in USDT0 atomic units (6 decimals). A cost parameter of `"1000"` translates to exactly $0.001. A cost of `"50000"` is $0.05. This precision allows servers to set prices at fractions of a cent. #### Infrastructure On Stable, [Semantic Pay](https://x402.semanticpay.io) operates a public facilitator. Developers can point their middleware to this endpoint without running their own settlement infrastructure. x402 provides middleware for Express (`@x402/express`), Hono (`@x402/hono`), and Next.js (`@x402/next`). The pattern is the same across all frameworks: create a facilitator client, register the EVM scheme, and apply middleware. ### What makes it different Traditional API monetization requires user registration, API key management, usage tracking, billing cycles, and payment processor integration. With x402, the server attaches a payment handler to each endpoint, the client pays per request, and settlement completes within the same HTTP lifecycle. The server does not need to know who the client is, only that a valid payment was submitted. | **Aspect** | **Traditional (API key + billing cycle)** | **Stable (x402)** | | :----------------------- | :------------------------------------------------------------------------ | :---------------------------------------- | | Server-side setup | Registration, API keys, usage tracking, billing cycles, payment processor | x402 payment handler per endpoint | | Client onboarding | Account creation, API key issuance | None (wallet only) | | Billing model | Monthly or usage-based invoicing | Per-request settlement | | Client identity required | Yes (API key) | No (only valid payment) | | Settlement | End of billing cycle | Within request lifecycle (under 1 second) | | Minimum viable price | \~$0.30 (card processing floor) | $0.001 (USDT0 atomic units) | | Client type | Human users only (sign-up required) | Any wallet: humans, AI agents, scripts | **See also:** * [x402 (HTTP-Native Payments)](/en/explanation/x402) * [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009) * [Gas Waiver](/en/how-to/integrate-gas-waiver) ## Ramps On/off ramp partners connect Stable to global fiat systems, enabling users and businesses to move between USDT, local currencies, and payment rails. ### On/off ramp overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :----------------------------------------------------------------------- | :------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | | [**Onmeta**](https://onmeta.in/on-off-ramp) | On/Off Ramp | [https://docs.onmeta.in/](https://docs.onmeta.in/) | Compliant fiat-to-USDT + cross-border payouts | | [**Halliday**](https://halliday.xyz/) | On/Off Ramp | [https://docs.halliday.xyz/pages/home](https://docs.halliday.xyz/pages/home) | CEX + Stripe integration | | [**Alchemy Pay**](https://alchemypay.org/about) | Gateway | [https://alchemypay.readme.io/docs/alchemypay-on-ramp](https://alchemypay.readme.io/docs/alchemypay-on-ramp) | 300+ payment methods | | [**DFX**](https://www.dfx.swiss/) | Off-Ramp FX | [https://docs.dfx.swiss/](https://docs.dfx.swiss/) | Regulated stablecoin-to-fiat | | [**Onramp Money**](https://onramp.money/) | On/Off Ramp | [https://docs.onramp.money/onramp/](https://docs.onramp.money/onramp/) | Emerging market coverage | | [**MoonPay**](https://www.moonpay.com/business/onramps) | Universal Ramp | [https://dev.moonpay.com/docs/on-ramp-overview](https://dev.moonpay.com/docs/on-ramp-overview) | Global card + bank support | | [**Transak**](https://transak.com/off-ramp) | On/Off Ramp | [https://docs.transak.com/](https://docs.transak.com/) | 450+ integrations | | [**Banxa**](https://banxa.com/solutions/by-use-case/on-and-off-ramping/) | Regulated Ramp | [https://docs.banxa.com/docs/overview](https://docs.banxa.com/docs/overview) | Local rails in 100+ countries | | [**Simplex by Nuvei**](https://www.simplex.com/) | On-Ramp | [https://buy.simplex.com](https://buy.simplex.com) | Card, Apple/Google Pay, SEPA, ACH across 245+ markets; powers downstream wallets including Atomic Wallet | ### Category guide * **On/Off Ramps:** Platforms enabling direct conversion between fiat currencies and USDT on Stable. * **Payment Gateways:** Services connecting apps, merchants, or fintechs to global fiat rails for seamless transactions. * **FX & Off-Ramp Networks:** Regulated infrastructure providing compliant, stablecoin-to-fiat settlement at scale. ### On/off ramps & payment gateways #### MoonPay Universal crypto on/off ramp used globally for instant asset purchases. **Capabilities** * Card + bank + local payment rails * Fast global onboarding * Multi-chain support **Get started**: Follow the [MoonPay on-ramp integration guide](https://dev.moonpay.com/docs/on-ramp-overview) to embed fiat-to-USDT purchasing on Stable into your app. #### Transak On/off ramp providing frictionless ways to buy, sell, and transfer crypto across 450+ apps. **Capabilities** * Global fiat methods * Easy integration APIs * Country-level compliance support **Get started**: Review the [Transak integration docs](https://docs.transak.com/) to add on/off ramp widgets or APIs with Stable as a supported network. #### Onmeta On/off ramp and cross-border payout infrastructure with built-in compliance for VDA platforms. **Capabilities** * Fiat ↔ USDT conversion * Compliance-ready flows * Cross-border payouts * Local settlement rails **Get started**: Refer to the [Onmeta API documentation](https://docs.onmeta.in/) to integrate fiat-to-USDT conversion and cross-border payouts on Stable. #### Halliday End-to-end payment suite enabling seamless on/off ramping from top CEXs and payment platforms like Coinbase and Stripe. **Capabilities** * Direct connection to major CEXs * Stripe + payment rail integration * Merchant and app-friendly flows **Get started**: Read the [Halliday integration docs](https://docs.halliday.xyz/pages/home) to connect CEX and Stripe-based payment flows to Stable in your product. #### Alchemy Pay Global payment gateway bridging traditional finance and crypto with 300+ payment methods. **Capabilities** * Global fiat on/off ramps * Merchant payments * Bank transfers + cards + local rails **Get started**: Use the [Alchemy Pay on-ramp API](https://alchemypay.readme.io/docs/alchemypay-on-ramp) to add 300+ payment methods for USDT purchases on Stable. #### DFX Swiss-regulated decentralized FX and off-ramp network with over 100M transactions. **Capabilities** * Stablecoin-to-fiat conversion * Regulated FX layer * Deep multi-currency support **Get started**: Consult the [DFX API documentation](https://docs.dfx.swiss/) to set up compliant stablecoin-to-fiat off-ramp flows using Stable as the source network. #### Onramp Money Low-cost fiat-to-crypto gateway specializing in emerging markets. **Capabilities** * Local payment method coverage * Instant settlement * 1.3M+ supported users **Get started**: Follow the [Onramp Money developer guide](https://docs.onramp.money/onramp/) to integrate fiat-to-crypto flows for emerging market users on Stable. #### Banxa Regulated on/off ramp infrastructure connecting users to crypto via local rails in 100+ countries. **Capabilities** * Regulated fiat gateways * Broad global coverage * Bank and local payment options **Get started**: Read the [Banxa integration overview](https://docs.banxa.com/docs/overview) to enable regulated on/off ramp flows with Stable as a supported network. #### Simplex by Nuvei Fiat-to-crypto on-ramp from Nuvei, supporting card, bank, and local payment methods across 245+ markets. Buying USDT on Stable through Simplex is also available inside any wallet or app that integrates the Simplex widget, including [Atomic Wallet](/en/reference/wallets#atomic-wallet). **Capabilities** * USDT on Stable purchases via Visa, Mastercard, Apple Pay, Google Pay, SEPA, and ACH * Global coverage across 245+ markets * White-label widget used by downstream wallets and apps (e.g., Atomic Wallet) **Get started**: Go to [https://buy.simplex.com](https://buy.simplex.com) to buy USDT on Stable directly, or integrate the [Simplex widget](https://www.simplex.com/) to offer on-ramp purchases inside your own app. *** Have an on/off ramp integrating Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## RPC providers RPC and developer infrastructure providers supporting Stable. ### Overview table | **Provider** | **Category** | **Docs / Get Started** | **Notes** | | :--------------------------------------------------------------------------------------------------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------- | | [**Alchemy**](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC + developer platform | [Get started with Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC, WebSocket, monitoring, SDKs | | [**Tenderly**](https://tenderly.co/) | Simulation + debugging | [tenderly.co](https://tenderly.co/) | Real-time simulation, tracing, transaction workflows | ### Alchemy A complete blockchain development platform trusted globally. [Get started with Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) **Capabilities** * RPC + WebSocket infrastructure * Monitoring dashboards * Developer APIs and SDKs **Get started**: Create a Stable app in the [Alchemy dashboard](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) to get an RPC URL, then use it as your JSON-RPC endpoint. ### Tenderly A full-stack developer platform offering simulation, debugging, monitoring, and execution tooling. **Capabilities** * Real-time contract simulation * Debugging and tracing * Transaction workflows for devs **Get started**: Set up a Stable project in the [Tenderly dashboard](https://tenderly.co/) to access simulation, debugging, and transaction tracing for your contracts. *** Have an RPC or infrastructure integration with Stable? Reach out at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz). ## SDK reference Full surface of `@stablechain/sdk`. For a walkthrough, see [SDK quickstart](/en/tutorial/sdk-quickstart). ### Install ```bash npm install @stablechain/sdk viem ``` ```text added 2 packages, audited 3 packages in 2s ``` `viem >= 2.0.0` is a peer dependency. ### `createStable(config)` Construct a `StableClient`. Returns an object with the methods listed under [`StableClient`](#stableclient). ```ts import { createStable, Network } from "@stablechain/sdk"; const stable = createStable({ network: Network.Mainnet, account }); ``` ```text StableClient { transfer, quoteBridge, bridge, quoteSwap, swap } ``` #### `StableConfig` | **Field** | **Type** | **Default** | **Description** | | :------------- | :------------------ | :----------------------- | :------------------------------------------------------------------------ | | `network` | `Network` | `Network.Mainnet` | Target network. | | `rpc` | `string` | Public RPC for `network` | RPC override. | | `account` | `viem.Account` | | Server-side signer (e.g. `privateKeyToAccount`). | | `transport` | `viem.Transport` | | Browser-wallet transport (e.g. `custom(window.ethereum)`). | | `walletClient` | `viem.WalletClient` | | Pre-built wallet client. Takes precedence over `account` and `transport`. | Provide one of `account`, `transport`, or `walletClient`. ### `StableClient` #### `transfer(params)` Send native USDT0 or any ERC-20 on Stable. Switches the wallet to the Stable chain, fetches token decimals on-chain when missing, and waits for the receipt. ```ts const { txHash } = await stable.transfer({ from: "0xYourAddress", to: "0xRecipient", amount: 10, }); ``` ```text { txHash: "0x8f3a...2d41" } ``` | **Param** | **Type** | **Description** | | :-------------- | :-------- | :---------------------------------------------- | | `from` | `string` | Sender address. | | `to` | `string` | Recipient address. | | `amount` | `number` | Human-readable amount. | | `token` | `string?` | ERC-20 contract address. Omit for native USDT0. | | `tokenDecimals` | `number?` | Decimals. Fetched on-chain when omitted. | Returns `OperationResult` (`{ txHash, toAmount? }`). #### `quoteBridge(params)` Preview a bridge. Read-only — no signature, no gas. ```ts const quote = await stable.quoteBridge({ fromChain: Chain.Ethereum, toChain: Chain.Stable, fromToken: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, }); ``` ```text { toAmount: 99.94 } ``` Returns `BridgeQuote`. #### `bridge(params)` Bridge tokens cross-chain. The SDK picks the route: LayerZero for USDT0 → USDT0, LI.FI for everything else. Pass a pre-fetched `quote` to skip the internal quote call. ```ts const { txHash } = await stable.bridge({ ...bridgeParams, quote }); ``` ```text { txHash: "0xabcd...7890" } ``` | **Param** | **Type** | **Description** | | :------------- | :------------- | :-------------------------------------------- | | `fromChain` | `Chain` | Source chain. | | `toChain` | `Chain` | Destination chain. | | `fromToken` | `string` | Source token contract address. | | `toToken` | `string` | Destination token contract address. | | `amount` | `number` | Human-readable amount. | | `fromDecimals` | `number?` | Source token decimals. Defaults to `6`. | | `recipient` | `string?` | Destination address. Defaults to signer. | | `quote` | `BridgeQuote?` | Pre-fetched quote. Skips internal quote call. | #### `quoteSwap(params)` Fetch a LI.FI swap quote on Stable. Returns a pre-built transaction request and the approval address. ```ts const quote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, }); ``` ```text { toAmount: 99.81, fromAmount: 100000000n, fromToken: "0x8a2B...", approvalAddress: "0x...", transactionRequest: { ... } } ``` #### `swap(params)` Swap tokens on Stable via LI.FI. Handles ERC-20 approval automatically and switches the wallet's chain when needed. ```ts const { txHash, toAmount } = await stable.swap({ ...swapParams, quote }); ``` ```text { txHash: "0xabcd...", toAmount: 99.81 } ``` | **Param** | **Type** | **Default** | **Description** | | :------------- | :----------- | :---------- | :----------------------------------- | | `fromToken` | `string` | | Source token address. | | `toToken` | `string` | | Destination token address. | | `amount` | `number` | | Human-readable amount. | | `fromDecimals` | `number?` | `6` | Source token decimals. | | `toAddress` | `string?` | signer | Recipient address. | | `quote` | `SwapQuote?` | | Pre-fetched quote. Skips LI.FI call. | ### Enums #### `Network` | **Value** | **Chain ID** | | :---------------- | :----------- | | `Network.Mainnet` | 988 | | `Network.Testnet` | 2201 | #### `Chain` Used by `quoteBridge` and `bridge`. Each entry has a corresponding `CHAIN_CONFIGS` entry. | **Enum** | **Network** | **Chain ID** | | :-------------------- | :--------------- | :----------- | | `Chain.Sepolia` | Ethereum Sepolia | 11155111 | | `Chain.StableTestnet` | Stable Testnet | 2201 | | `Chain.Stable` | Stable Mainnet | 988 | | `Chain.Ethereum` | Ethereum | 1 | | `Chain.Arbitrum` | Arbitrum One | 42161 | | `Chain.Ink` | Ink | 57073 | | `Chain.Bera` | Berachain | 80094 | | `Chain.MegaETH` | MegaETH | 4326 | | `Chain.Base` | Base | 8453 | | `Chain.BSC` | BNB Smart Chain | 56 | | `Chain.HyperEVM` | HyperEVM | 999 | ### `CHAIN_CONFIGS` `Partial>` keyed by `Chain` enum. Each entry exposes `id`, `rpc`, `usdt`, and `decimals`. Use it when you need the canonical USDT address on a supported chain without hard-coding it. ```ts import { CHAIN_CONFIGS, Chain } from "@stablechain/sdk"; console.log(CHAIN_CONFIGS[Chain.Stable]); ``` ```text { id: 988, rpc: "https://rpc.stable.xyz", usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", decimals: 6 } ``` ### Errors All SDK errors extend `StableError`, which extends viem's `BaseError`. Errors carry structured metadata so you can branch on `error.name` or `instanceof`. | **Class** | **Thrown when** | **Useful fields** | | :----------------------- | :-------------------------------------------------------------------------------- | :----------------------------------------------- | | `StableValidationError` | A parameter fails validation (bad address, non-finite amount, unsupported chain). | `field`, `value` | | `StableQuoteError` | A quote request to LI.FI fails. | `provider`, `httpStatus`, `providerCode`, `body` | | `StableTransactionError` | On-chain step fails: chain switch, approval, send, or revert. | `phase`, `txHash`, `chainId`, `revertReason` | | `StableNetworkError` | An underlying HTTP/RPC call fails. | `url` | ```ts import { StableTransactionError } from "@stablechain/sdk"; try { await stable.transfer({ from, to, amount: 1 }); } catch (err) { if (err instanceof StableTransactionError && err.phase === "switch_chain") { // user rejected the chain switch } throw err; } ``` ```text StableTransactionError: transfer: wallet rejected or failed to switch to chain 988 Phase: switch_chain Chain ID: 988 ``` ### Next recommended * [**SDK quickstart**](/en/tutorial/sdk-quickstart) — Install the SDK and run your first transfer on testnet. * [**Use with viem**](/en/how-to/sdk-with-viem) — Switch between private-key, browser-wallet, and pre-built signers. * [**Use with wagmi**](/en/how-to/sdk-with-wagmi) — Wire the SDK into a React app with hooks. ## Staking precompile reference :::note **Concept:** For what the staking module does and when to use it, see [Staking module](/en/explanation/staking-module). ::: ### Abstract The `staking` precompiled contract acts as a bridge that enables EVM environments to use the Stable SDK's `x/staking` module functionality. ### Contents 1. **[Concepts](#concepts)** 2. **[Configuration](#configuration)** 3. **[Methods](#methods)** 4. **[Events](#events)** ### Concepts In `x/staking` module in Stable SDK, bond denom must be registered during chain initialization for staking. Validators and delegators can only use the bond denom staking token. The `staking` precompiled contract performs additional checks to ensure that the validator or delegator is the caller. ### Configuration The contract address and gas cost are predefined. #### Contract address * `0x0000000000000000000000000000000000000800` ### Methods #### `createValidator` Creates a validator. The validator must be created with an initial delegation from the operator. For potential delegators, the validator should offer information and a plan for the commission rate. Delegators can choose a validator to delegate their tokens to based on disclosed information, with natural regulation from market mechanisms. `CreateValidator` is emitted when the validator is successfully registered. ##### Inputs | Name | Type | Description | | ----------------- | --------------- | ------------------------------------------------------------------------- | | description | Description | information of the validator | | commissionRates | CommissionRates | commission rate of the staking token rewarded by the validator | | minSelfDelegation | uint256 | the minimum self-delegation amount of the validator | | validatorAddress | address | the address of the validator | | pubkey | string | the public key of the validator | | value | uint256 | the amount of the staking token initially self-delegated to the validator | `Description` is a struct with the following fields: | Name | Type | Description | | --------------- | ------ | --------------------------------------- | | moniker | string | name of the validator | | identity | string | the identity of the validator | | website | string | the url of the validator's website | | securityContact | string | security contract information | | details | string | additional description of the validator | `CommissionRates` is a struct with the following fields: | Name | Type | Description | | ------------- | ------- | --------------------------------------------------------------- | | rate | uint256 | current commission rate that the validator receives | | maxRate | uint256 | the maximum commission rate (cannot be set higher than this) | | maxChangeRate | uint256 | the maximum commission rate that a validator can change per day | `rate` should be set to an appropriate value acceptable to the market. * If validator's commission rate is higher, delegator's profit is lower. * If validator's commission rate is lower, validator's profit is lower and it makes operation difficult. Since a high `maxRate` can make delegators concerned about an unexpected high commission rate from the validator, `maxRate` should be set carefully. `maxChangeRate` is unchangeable when initialized. ##### Outputs | Name | Type | Description | | ------- | ---- | ------------------------------------------------ | | success | bool | true if the validator is successfully registered | #### `editValidator` Validator updates its information. Validator only can update information except unchangeable fields such as `maxRate` and `maxChangeRate` in `CommissionRates` struct. `EditValidator` is emitted when the validator is successfully updated. ##### Inputs | Name | Type | Description | | ----------------- | ----------- | ------------------------------------------------------------------ | | description | Description | information of the validator | | validatorAddress | address | the address of the validator | | commissionRate | int256 | the commission rate of the staking token rewarded by the validator | | minSelfDelegation | int256 | the minimum self-delegation amount of the validator | ##### Outputs | Name | Type | Description | | ------- | ---- | --------------------------------------------- | | success | bool | true if the validator is successfully updated | #### `delegate` Delegator sets the amount of token to be delegated to the validator. `Delegate` is emitted when the delegation is successfully done. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------------------------------------- | | delegatorAddress | address | the address of the delegator | | validatorAddress | address | the address of the validator | | amount | uint256 | the amount of the staking token delegated to the validator | ##### Outputs | Name | Type | Description | | ------- | ---- | ------------------------------------------- | | success | bool | true if the delegation is successfully done | ##### Events `newShares` indicates the ownership ratio of the delegator. The shares calculated may vary depending on the time even though the same amount of tokens are delegated. #### `undelegate` Delegator withdraws the amount of token delegated to the validator. `Unbond` is emitted when the undelegation is successfully done. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ------------------------------------------------------------------------ | | delegatorAddress | address | the address of the delegator | | validatorAddress | address | the address of the validator | | amount | uint256 | the amount of the staking token willing to undelegate from the validator | ##### Outputs | Name | Type | Description | | ------- | ---- | --------------------------------------------- | | success | bool | true if the undelegation is successfully done | #### `redelegate` Delegator redelegates the amount of token delegated to the validator to another validator. `Redelegate` is emitted when the redelegate is successfully done. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ------------------------------------------------ | | delegatorAddress | address | the address of the delegator | | validatorSrc | string | the address of the source validator | | validatorDst | string | the address of the destination validator | | amount | uint256 | the amount of the staking token for redelegation | ##### Outputs | Name | Type | Description | | ------- | ---- | ------------------------------------------- | | success | bool | true if the redelegate is successfully done | #### `delegation` Returns the delegation information between a delegator and a validator. If there is no delegation found, the `shares` and `balance` will be `0`. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | delegatorAddress | address | the address of the delegator | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ------- | ------- | --------------------------------------- | | shares | uint256 | the delegated shares | | balance | Coin | the amount of delegated token and denom | `Coin` is a struct with the following fields: | Name | Type | Description | | ------ | ------- | ------------------------ | | denom | string | the denom of the reward | | amount | uint256 | the amount of the reward | #### `unbondingDelegation` Returns the unbonding delegation information between a delegator and a validator. If there is no unbonding delegation found, empty `UnbondingDelegationOutput` will be returned. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | delegatorAddress | address | the address of the delegator | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | ------------------- | ------------------------- | ------------------------------------------- | | unbondingDelegation | UnbondingDelegationOutput | the information of the unbonding delegation | `UnbondingDelegationOutput` is a struct with the following fields: | Name | Type | Description | | ---------------- | --------------------------- | --------------------------------------- | | validatorAddress | address | the address of the validator | | delegatorAddress | address | the address of the delegator | | entries | UnbondingDelegationEntry\[] | the entries of the unbonding delegation | `UnbondingDelegationEntry` is a struct with the following fields: | Name | Type | Description | | -------------- | ------ | -------------------------------- | | creationHeight | uint64 | the creation height of the entry | | completionTime | uint64 | the completion time of the entry | | initialBalance | Coin | the initial balance of the entry | | balance | Coin | the balance of the entry | #### `validator` Returns the validator information. If there is no validator found, empty `ValidatorOutput` will be returned. ##### Inputs | Name | Type | Description | | ---------------- | ------- | ---------------------------- | | validatorAddress | address | the address of the validator | ##### Outputs | Name | Type | Description | | --------- | --------- | -------------------------------- | | validator | Validator | the information of the validator | `Validator` is a struct with the following fields: | Name | Type | Description | | ----------------- | ------- | ------------------------------------------------------------------ | | operatorAddress | address | the address of the validator | | consensusPubkey | string | the public key of the validator | | jailed | bool | whether the validator is jailed or not | | status | int32 | the status of the validator | | tokens | uint256 | the amount of the staking token delegated to the validator | | delegatorShares | uint256 | the amount of the delegation shares | | description | string | the description of the validator | | unbondingHeight | int64 | the height at which the validator is unbonding | | unbondingTime | int64 | the time at which the validator is unbonding | | commission | uint256 | the commission rate of the staking token rewarded by the validator | | minSelfDelegation | uint256 | the minimum self-delegation amount of the validator | #### `validators` Returns all validators matched with the status. If there is no validator found, empty `ValidatorsOutput` will be returned. Status, declared in `x/staking` module, can be one of the following: * 0 : "BOND\_STATUS\_UNSPECIFIED", unspecified status * 1 : "BOND\_STATUS\_UNBONDING", validator is unbonding * 2 : "BOND\_STATUS\_UNBONDED", validator is unbonded * 3 : "BOND\_STATUS\_BONDED", validator is bonded ##### Inputs | Name | Type | Description | | ----------- | ------- | --------------------------- | | status | string | the status of the validator | | pageRequest | PageReq | request for pagination | `PageReq` is a struct with the following fields: | Name | Type | Description | | ---------- | ----- | --------------------------------------------------- | | key | bytes | the key of the page | | offset | int64 | the offset of the page | | limit | int64 | the limit of the page | | countTotal | bool | whether to count the total number of results or not | | reverse | bool | whether to reverse the results or not | ##### Outputs | Name | Type | Description | | ------------ | ------------ | ---------------------------- | | validators | Validator\[] | the arrays of the validators | | pageResponse | PageResp | response for pagination | `PageResp` is a struct with the following fields: | Name | Type | Description | | ------- | ------ | ------------------------------- | | nextKey | bytes | the next key of the page | | total | uint64 | the total number of the results | #### `redelegation` Returns the redelegation information of delegator, source validator and destination validator. If there is no redelegation found, empty `RedelegationOutput` will be returned. ##### Inputs | Name | Type | Description | | ------------------- | ------- | ---------------------------------------- | | delegatorAddress | address | the address of the delegator | | srcValidatorAddress | address | the address of the source validator | | dstValidatorAddress | address | the address of the destination validator | ##### Outputs | Name | Type | Description | | ------------ | ------------------ | ----------------------------------- | | redelegation | RedelegationOutput | the information of the redelegation | `RedelegationOutput` is a struct with the following fields: | Name | Type | Description | | ------------------- | -------------------- | ---------------------------------------- | | delegatorAddress | address | the address of the delegator | | validatorSrcAddress | address | the address of the source validator | | validatorDstAddress | address | the address of the destination validator | | entries | RedelegationEntry\[] | the entries of the redelegation | `RedelegationEntry` is a struct with the following fields: | Name | Type | Description | | -------------- | ------ | -------------------------------- | | creationHeight | uint64 | the creation height of the entry | | completionTime | uint64 | the completion time of the entry | | initialBalance | Coin | the initial balance of the entry | | balance | Coin | the balance of the entry | #### `redelegations` Returns all redelegations of delegator, source validator and destination validator. If there is no redelegation found, empty `RedelegationResponse` and `PageResp` will be returned. ##### Inputs | Name | Type | Description | | ------------------- | ------- | ---------------------------------------- | | delegatorAddress | address | the address of the delegator | | srcValidatorAddress | address | the address of the source validator | | dstValidatorAddress | address | the address of the destination validator | | pageRequest | PageReq | request for pagination | ##### Outputs | Name | Type | Description | | ------------ | ----------------------- | ------------------------------------ | | response | RedelegationResponse\[] | the information of the redelegations | | pageResponse | PageResp | response for pagination | ### Events #### CreateValidator | Name | Type | Indexed | Description | | -------- | ------- | ------- | ------------------------------------------------------------------------- | | valiAddr | address | Y | the address of the validator | | value | uint256 | N | the amount of the staking token initially self-delegated to the validator | #### EditValidator | Name | Type | Indexed | Description | | ----------------- | ------- | ------- | -------------------------------------------------------------------------- | | valiAddr | address | Y | the address of the validator | | commissionRate | int256 | N | the updated commission rate of the staking token rewarded by the validator | | minSelfDelegation | int256 | N | the updated minimum self-delegation amount of the validator | #### Delegate | Name | Type | Indexed | Description | | ------------- | ------- | ------- | ---------------------------------------------------------- | | delegatorAddr | address | Y | the address of the delegator | | validatorAddr | string | Y | the address of the validator | | amount | uint256 | N | the amount of the staking token delegated to the validator | | newShares | uint256 | N | the amount of the delegation shares after the delegation | #### Unbond | Name | Type | Indexed | Description | | -------------- | ------- | ------- | -------------------------------------------------------------- | | delegatorAddr | address | Y | the address of the delegator | | validatorAddr | string | Y | the address of the validator | | amount | uint256 | N | the amount of the staking token undelegated from the validator | | completionTime | uint256 | N | the completion time of the undelegation | #### Redelegate | Name | Type | Indexed | Description | | ------------------- | ------- | ------- | ------------------------------------------------ | | delegatorAddr | address | Y | the address of the delegator | | validatorSrcAddress | address | Y | the address of the source validator | | validatorDstAddress | address | Y | the address of the destination validator | | amount | uint256 | N | the amount of the staking token for redelegation | | completionTime | uint256 | N | the completion time of the redelegate | ## Set up recurring billing Pull-based subscriptions let a service provider collect payments on a schedule without requiring the subscriber to initiate each payment. This pattern is enabled by [EIP-7702](/en/reference/eip-7702-api) account abstraction. The subscriber's EOA delegates execution authority to a subscription delegate contract, which the provider calls each billing cycle. The subscriber acts only twice: once to subscribe, once to cancel. ### How it works The subscriber delegates their EOA to a contract that enforces billing terms. Through EIP-7702, the subscriber's account temporarily gains contract logic, allowing a service provider to collect payments at each billing cycle without the subscriber signing every time. #### Subscription lifecycle 1. **Delegate**: the subscriber delegates their EOA to a subscription delegate contract via EIP-7702. 2. **Subscribe**: the subscriber registers billing terms: provider address, amount per interval, and billing interval. 3. **Collect**: the service provider triggers collection each billing cycle. The delegate contract verifies the caller, interval, and amount before executing the USDT0 transfer. 4. **Cancel**: the subscriber revokes the subscription or clears the delegation to stop future collections. #### Important considerations * **Persistent delegation**: The EIP-7702 delegation persists until the subscriber explicitly changes or clears it. No re-delegation is needed each billing cycle. * **Single delegation per EOA**: EIP-7702 supports one active delegation per EOA. If the subscriber later delegates to a different contract, the subscription delegate logic is replaced and collection fails. Use a modular delegate contract that supports multiple functions (subscriptions, batch payments, spending limits) under a single delegation. * **Use audited delegates**: A delegate contract has full execution authority over the subscriber's EOA. Only delegate to contracts that have been audited. ### What makes it different Traditional subscriptions store card data, retry failed charges, and manage complex billing state. With EIP-7702 subscriptions, the billing terms are enforced by the delegate logic on the subscriber's own EOA. The provider can only collect the agreed amount per interval, and the subscriber can cancel at any time by revoking the delegation. | **Aspect** | **Traditional (card-on-file)** | **Stable** | | :------------------ | :---------------------------------------- | :-------------------------------------- | | Setup | Card registration with payment processor | Single EIP-7702 delegation transaction | | Billing | Processor charges stored card | Provider calls delegate contract | | Stored payment data | Card number, CVV held by processor | No payment credentials stored off-chain | | Cancellation | Contact provider or card issuer | Subscriber revokes delegation on-chain | | Overcharge risk | Depends on provider-side billing controls | Billing terms enforced by contract | **See also:** * [EIP-7702](/en/reference/eip-7702-api) * [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009) ## System modules reference Stable exposes core settlement behavior through **System Modules**, implemented as **precompiled contracts** for gas efficiency and predictable control. :::note **Concept:** For what system modules do and why they're exposed as precompiles, see [System modules](/en/explanation/system-modules-overview). ::: **Key modules:** * [Bank Module](/en/reference/bank-module-api) * Handles USDT transfers, balance accounting, and escrow flows * [Distribution Module](/en/reference/distribution-module-api) * Fee distribution and reward logic for network participants * [Staking Module](/en/reference/staking-module-api) * Controls validator participation and staking (coming with mainnet) **dApps can leverage built-in modules instead of re-implementing token or settlement logic.** ## System transactions reference :::note **Concept:** For how system transactions bridge SDK events to the EVM and why this matters, see [System transactions](/en/explanation/system-transactions). ::: ### Abstract System transactions provide a way for the Stable protocol to emit EVM events for Stable SDK operations. When staking events like unbonding completions occur in the SDK layer, the protocol automatically generates EVM transactions that emit corresponding events. This makes these operations fully visible to EVM tooling and applications. ### Motivation You and your applications on Stable expect to monitor blockchain events through standard EVM interfaces like `eth_getLogs`. But critical operations happen in Stable SDK modules that don't naturally emit EVM events. This creates a visibility gap: EVM dApps can't easily track when a user's tokens finish unbonding. System transactions bridge this gap. When the staking module completes an unbonding operation, Stable's x/stable module detects the event and generates a system transaction that calls the StableSystem precompile ( `0x0000000000000000000000000000000000009999`). And then, the precompile emits proper EVM events that any dApp can subscribe to. System transactions run with a special sender address (`0x8888888888888888888888888888888888888888`) that only the protocol can use. This prevents anyone from spoofing protocol events while keeping the event emission trustless and verifiable on-chain. ### Specification System transactions work through three main components: the x/stable module's EndBlocker, the PrepareProposal handler, and the StableSystem precompile. #### Architecture overview system-transaction-architecture #### StableSystem precompile The StableSystem precompile lives at `0x0000000000000000000000000000000000009999` and handles protocol-level operations that need to emit EVM events. Currently it supports unbonding completion notifications. ```solidity interface IStableSystem { /// @notice Processes queued unbonding completions and emits EVM events /// @param blockHeight The block height at which to process completions /// @dev Only callable by system transactions (from = 0x8888888888888888888888888888888888888888) /// @dev Processes up to 100 completions per call /// @dev Automatically deletes processed completions from the queue function notifyUnbondingCompletions(int64 blockHeight) external; /// @notice Emitted when an unbonding operation completes /// @param delegator The address that delegated the tokens /// @param validator The validator address the tokens were delegated to /// @param amount The amount of tokens that finished unbonding (in uusdc) event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); /// @notice The caller is not authorized (not system transaction sender) error Unauthorized(); } ``` #### System transaction sender System transactions use `0x8888888888888888888888888888888888888888` as the sender address. This address: * Requires no signature verification * Can only be used by transactions created in PrepareProposal * Cannot be spoofed by users or contracts * Skips fee deduction via the SystemTxDecorator ante handler The EVM recognizes system transactions by checking `msg.sender == 0x8888888888888888888888888888888888888888`. Precompiles can use this to gate protocol-only operations. #### Event-driven flow When a user's unbonding period completes, here's what happens: 1. **Stable SDK Layer:** The staking module's EndBlocker completes the unbonding and emits EventTypeCompleteUnbonding with the delegator address, validator address, and amount. 2. **Detection:** The x/stable module's EndBlocker runs after staking and scans for unbonding events in the block's event log. For each completion, it queues an entry in state with the delegator address, validator address, amount, and block height. 3. **System TX Generation**: In the next block's PrepareProposal, the app queries all queued completions. If any exist, it creates a system transaction calling StableSystem.notifyUnbondingCompletions(blockHeight) with the current block height. This transaction goes at the front of the block, before any user transactions. 4. **Execution:** During block execution, the system transaction runs first. The precompile queries state for queued completions at that block height, emits an UnbondingCompleted event for each one (up to 100), and deletes them from the queue. 5. **EVM Visibility:** The events appear in transaction receipts and logs, visible to eth\_getLogs queries, block explorers, and any application monitoring the StableSystem precompile. #### Batch processing To prevent blocks from becoming too large, the system processes at most 100 unbonding completions per block. If 150 completions queue up: * Block N: Creates system tx processing completions 0-99 * Block N+1: Creates system tx processing completions 100-149 The precompile queries state directly rather than receiving completion data in calldata. This keeps transaction size predictable and moves the data from expensive calldata to cheaper state reads. ### Usage examples The most common use case is a staking dashboard that needs to notify users when their unbonding periods complete. Here's how to set up a listener for unbonding completions. ```javascript import { ethers } from 'ethers'; // StableSystem precompile address const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999'; // ABI for the UnbondingCompleted event const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; // Connect to the Stable network const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz'); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); // Subscribe to all unbonding completions stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => { console.log('Unbonding completed!'); console.log('Delegator:', delegator); console.log('Validator:', validator); console.log('Amount:', ethers.formatEther(amount), 'tokens'); console.log('Block:', event.log.blockNumber); console.log('Tx Hash:', event.log.transactionHash); }); ``` This listener fires every time any user's unbonding completes. For a production dApp, filter events for specific users as shown below. #### Filtering events for specific users To only receive events for a particular delegator address, use the indexed event parameters to create a filter: ```javascript // Only watch unbondings for a specific user const userAddress = '0xabcd...'; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { // This only fires for the specified user's unbondings showNotification(`Your unbonding of ${ethers.formatEther(amount)} tokens completed!`); refreshUserBalance(userAddress); }); ``` You can also filter by validator if you're building a validator-specific dashboard: ```javascript // Watch all unbondings from a specific validator const validatorAddress = '0x1234...'; const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### Querying historical events If your dApp needs to show a history of past unbonding completions, you can query historical events using event filters with block ranges: ```javascript // Get all unbondings for a user in the last 1000 blocks const currentBlock = await provider.getBlockNumber(); const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter( filter, currentBlock - 1000, currentBlock ); const unbondingHistory = events.map(event => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash })); console.log('Recent unbondings:', unbondingHistory); ``` ### Integration guide #### Step 1: Add the Stable System contract interface First, add the StableSystem precompile interface to your project. If you're using Foundry or Hardhat, create a new interface file: ```solidity interface IStableSystem { event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); } ``` If you're building a pure frontend dApp without Solidity contracts, you just need the ABI fragment for the event: ```javascript const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; ``` #### Step 2: Set up event listeners Initialize your ethers.js provider and create a contract instance pointing to the StableSystem precompile address. The precompile is always deployed at `0x00000000000....0000009999` on both Stable Testnet and Stable Mainnet. *Note: The precompile is not deployed on Stable Mainnet yet, it will be provided after v1.2.0 upgrade.* ```javascript const provider = new ethers.JsonRpcProvider(RPC_URL); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); ``` #### Step 3: Handle events in your application logic Subscribe to events and update your application state accordingly. Common patterns include: * **Balance Updates**: When an unbonding completes, refresh the user's token balance * **Notification System**: Show toast notifications when the user's unbondings complete * **Dashboard Statistics**: Update staking metrics and charts in real-time * **Transaction History**: Add completed unbondings to the user's activity feed #### Step 4: Handle connection issues Since event subscriptions rely on persistent websocket connections, implement reconnection logic for production dApps: ```javascript let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function setupEventListener() { const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz'); provider.on('error', (error) => { console.error('Provider error:', error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); stableSystem.on('UnbondingCompleted', handleUnbonding); } ``` ### Why this approach? #### Compared to custom indexers Previously, Stable SDK required you to run custom indexers that watch for SDK events and store them in a database. This adds operational overhead and introduces potential points of failure. With system transactions, there's no need for separate indexer infrastructure. Events are natively available through the EVM's log system, which every RPC node already indexes and serves. Any standard web3 library can subscribe to these events without additional tooling. #### Compared to polling SDK endpoints Without system transactions, EVM dApps would need to periodically call Stable SDK REST endpoints to check if unbonding periods have completed. This creates several problems: * **Increased latency**: Polling intervals of 5-10 seconds mean users might wait that long before seeing updates * **Higher load**: Every dApp instance polling endpoints increases load on RPC infrastructure * **Complexity**: dApps need to handle both web3 providers (for EVM interactions) and Stable SDK REST clients (for SDK queries) * **No real-time updates**: Polling inherently can't provide instant notifications System transactions provide real-time event notifications through the same websocket connections dApps already use for EVM interactions. This simplifies the developer experience and reduces infrastructure costs. ### Security guarantees #### Trustless event emission System transactions are created during the `PrepareProposal` ABCI phase, which only validators can execute. User-submitted transactions cannot spoof the system sender address (`0x8888888888888888888888888888888888888888`). The EVM's state transition logic enforces that only transactions to the StableSystem precompile address can skip signature verification. This means: * Users cannot forge unbonding completion events * Users cannot call `notifyUnbondingCompletions` from their own transactions * The only way to emit an `UnbondingCompleted` event is for an actual unbonding to complete in the Stable SDK staking module #### No additional trust assumptions System transactions don't introduce new security assumptions beyond what's already required for blockchain consensus. If you trust that validators are correctly executing blocks, you can trust that system transaction events accurately reflect Stable SDK state changes. The event emission process is deterministic: given the same SDK events in `EndBlock`, all honest validators will produce identical system transactions during `PrepareProposal`. The consensus mechanism ensures validators agree on which system transactions to include. #### Block finality The Stable blockchain uses fast finality through StableBFT's consensus mechanism. Once a block is committed, it's immediately final and cannot be reorganized. This means that once you receive an `UnbondingCompleted` event, you can trust it's permanent. There's no need to wait for multiple confirmations like on probabilistic finality chains. dApps can update user balances and display notifications immediately upon receiving the event. ### Performance & limitations #### Batch size constraints Each block processes at most 100 unbonding completions through system transactions. This limit exists to prevent unbounded block sizes during periods of high unbonding activity. In practice, 100 completions per block provides throughput of \~9000 completions per minute assuming the average block time of 0.7 seconds. Normal staking activity rarely reaches this limit. During exceptional circumstances, completions might queue for several blocks before fully processing. #### Gas consumption System transactions consume gas during execution, which is accounted for in the block's gas limit. The gas cost scales linearly with the number of completions being processed: * Base function call: \~21,000 gas * Per-event emission: \~3,000 gas * Reading state: \~2,000 gas per completion A full batch of 100 completions consumes approximately 521,000 gas. As Stable’s block gas limit is 100,000,000, this represents less than 0.6% of available block space. #### Notification latency When an unbonding period completes during block N: 1. The Stable module's `EndBlock` queues the completion in block N's state 2. Block N+1's `PrepareProposal` creates a system transaction 3. The system transaction executes during block N+1, emitting the event This means there's a one-block delay (approximately 0.7 seconds) between the unbonding completing and the EVM event being emitted. For most use cases, this latency is acceptable since the unbonding period itself is 7 days. #### High load scenarios If unbonding completions arrive faster than 100 per block, they accumulate in the queue. The queue is processed in FIFO order, so the oldest completions are always notified first. During sustained high load, the queue could grow temporarily. However, once the spike subsides, subsequent blocks with fewer completions will gradually drain the queue. The system is designed to handle bursts without dropping events. ### Future extensions The system transaction mechanism provides a general pattern for bridging any Stable SDK operation into the EVM event space. While currently used only for unbonding completions, the architecture can be extended to cover additional use cases: #### Staking operations Beyond unbonding, other staking events could emit EVM notifications: * Commission rate changes by validators * Validator jailing and unjailing #### Governance execution When governance proposals pass and execute, system transactions could emit events with proposal IDs and execution results. This would allow dApps to react to parameter changes or upgrades without polling the governance module. #### Generic event bridge The pattern could be generalized into a configurable event bridge where each module registers which SDK events should be mirrored to the EVM. This would provide comprehensive visibility into all Stable SDK operations without requiring per-module custom logic. The key architectural principle is that system transactions remain a protocol-level feature, created only by validators during block proposal. ## Ecosystem In this document, you can find the information for bridge (LayerZero) and USDT0. ### LayerZero on Stable Testnet | Name | Value | | ----------------- | ------------------------------------------ | | eid | 40374 | | chainKey | stable-testnet | | stage | testnet | | endpointV2View | 0x6Ac7bdc07A0583A362F1497252872AE6c0A5F5B8 | | endpointV2 | 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 | | sendUln302 | 0x9eCf72299027e8AeFee5DC5351D6d92294F46d2b | | receiveUln302 | 0xB0487596a0B62D1A71D0C33294bd6eB635Fc6B09 | | blockedMessageLib | 0xa229b65cc2191bf60bc24efcda3487d7b5c0c9f0 | | executor | 0x701f3927871EfcEa1235dB722f9E608aE120d243 | | deadDVN | 0xC1868e054425D378095A003EcbA3823a5D0135C9 | ### USDT0 on Stable Testnet | Name | Value | | ----------- | ------------------------------------------ | | wrapper | 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb | | composer | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D | | OFT | 0x779Ded0c9e1022225f8E0630b35a9b54bE713736 | | USDT0 impl | 0x3f9E27457ac494fC729beB50e6af04Ec34e3828E | | USDT0 proxy | 0x78Cf24370174180738C5B8E352B6D14c83a6c9A9 | ### Sepolia OFT contract and USDT0 contract (for reference) | Name | Value | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sepolia OFT | [https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126](https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126) | | Sepolia USDT | [https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract](https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) | ## Testnet information Everything you need to know to access the Stable Testnet. ### Network overview | Configuration | Value | | ---------------- | -------------- | | **Network Name** | Stable Testnet | | **Chain ID** | `2201` | | **Gas Token** | USDT0 | | **Gov Token** | STABLE | | **Block Time** | \~0.7 seconds | ### Block explorers | Explorer | URL | | -------------- | ---------------------------------------------------------------- | | **Stablescan** | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) | ### RPC endpoints #### Primary endpoints | Type | Endpoint | Purpose | | ---------------- | ---------------------------------------------------------------- | ----------------- | | **EVM JSON-RPC** | [https://rpc.testnet.stable.xyz](https://rpc.testnet.stable.xyz) | EVM transactions | | **WebSocket** | wss\://rpc.testnet.stable.xyz | Real-time updates | :::note The public RPC endpoint is rate-limited to **1,000 requests per 10 seconds per IP**. Requests over the limit return `HTTP 429`. For higher throughput, use a [third-party RPC provider](/en/reference/rpc-providers). ::: ### Chain information | Parameter | EVM | | ------------------ | ------- | | **Chain ID** | `2201` | | **Address Format** | `0x...` | | **Gas Token** | `USDT0` | | **Decimals** | 18 | ### Faucet & tools | Tool | URL | Description | | ------------- | --------------------------------------------------------- | --------------- | | **Faucet** | [https://faucet.stable.xyz](https://faucet.stable.xyz) | Get test tokens | | **Snapshots** | See [Node Operators Guide](/en/how-to/use-node-snapshots) | Chain snapshots | ## Version history Complete version history and related documentation for the Stable Testnet. ### Current version information * **Current Version**: `v1.4.0-rc0` * **Next Upgrade**: `TBD` * **Upgrade Height**: `TBD` * **Expected Time**: `TBD` ### Version history #### Current & previous versions | Version | Commit | Upgrade Height | Binary | Status | | -------------- | ---------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | **v1.4.0-rc0** | `83b5efb` | 57,806,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.4.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.4.0-rc0-linux-arm64-testnet.tar.gz) | Current | | **v1.3.1-rc0** | `75bb546` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.1-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.1-rc0-linux-arm64-testnet.tar.gz) | | | **v1.3.0-rc1** | `25b5e47` | 53,265,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc1-linux-arm64-testnet.tar.gz) | | | **v1.3.0-rc0** | `864d54c` | 49,855,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-arm64-testnet.tar.gz) | | | **v1.2.2-rc0** | `8bd5d5e` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-arm64-testnet.tar.gz) | | | **v1.2.1-rc1** | `7ff9a8a` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-arm64-testnet.tar.gz) | | | **v1.2.0-rc1** | `263c033` | 41,306,450 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz) | | | **v1.2.0** | `ee8ca35` | 40,392,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-arm64-testnet.tar.gz) | | | **v1.1.2** | `3d83aa3` | 34,649,300 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-arm64-testnet.tar.gz) | | | **v1.1.1** | `8becd6b` | 33,152,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-arm64-testnet.tar.gz) | | | **v1.1.0** | `17ceaa7` | 32,309,700 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-arm64-testnet.tar.gz) | | | **v0.8.1** | `1eb65d5` | 30,770,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-arm64-testnet.tar.gz) | | | **v0.8.0** | `e55efb6` | 29,410,999 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-arm64-testnet.tar.gz) | Bank precompile enhancements | | **v0.7.2** | `3c53e14` | 27,258,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-arm64-testnet.tar.gz) | StableBFT integration | | **v0.6.0** | `5cc1ad6` | 19,587,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.6.0-linux-amd64-testnet.tar.gz) | Minor fixes | | **v0.5.0** | `919281d` | 18,719,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.5.0-linux-amd64-testnet.tar.gz) | Minor fixes | | **v0.4.0** | `c6240c0` | 18,666,150 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.4.0-linux-amd64-testnet.tar.gz) | Stable SDK v0.53.4 | | **v0.3.0** | `a4f5ac5` | 9,166,131 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.3.0-linux-amd64-testnet.tar.gz) | EVM value transfer allow list | | **v0.2.1** | `53e6e073` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.1-linux-amd64-testnet.tar.gz) | Non-breaking update | | **v0.2.0** | `8bdd771` | 8,956,584 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.0-linux-amd64-testnet.tar.gz) | Feature update | | **v0.1.0** | `10dfg542` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.1.0-linux-amd64-testnet.tar.gz) | Genesis (2025-04-07) | ### Related documentation * [Upgrade Guide](/en/how-to/upgrade-node) - Step-by-step upgrade procedures * [Testnet Information](/en/reference/testnet-information) - Current network details ## Tokenomics STABLE is the governance token of the Stable Mainnet. It secures the network through delegated Proof-of-Stake, governs protocol upgrades, and entitles stakers to a share of USDT0 gas revenue distributed by validators. ### Overview | Item | Details | | :--------------- | :----------------------------- | | **Symbol** | STABLE | | **Total supply** | 100,000,000,000 tokens | | **Standard** | ERC-20 (on Stable Mainnet EVM) | | **Decimals** | 18 | STABLE is the governance token of the Stable Mainnet and Ecosystem, designed to support long-term economic alignment across validators, developers, and users. *** ### Token allocation **Total supply:** 100,000,000,000 STABLE tokens | Category | Allocation | Amount of STABLE | | :------------------------ | :--------- | :--------------- | | **Investors & advisors** | 25% | 25,000,000,000 | | **Team** | 25% | 25,000,000,000 | | **Ecosystem & community** | 40% | 40,000,000,000 | | **Genesis distribution** | 10% | 10,000,000,000 | | **Total** | 100% | 100,000,000,000 | *** ### Emission model & supply schedule * Total supply is fixed at 100,000,000,000 STABLE tokens. * Only a portion of supply enters circulation at launch of the Stable Mainnet. * Team and Investors & advisors allocations follow a 4-year linear vesting model, with a 1-year cliff, to ensure long-term commitment. *** ### Allocations #### Genesis distribution - 10% of total token supply * Designed to bootstrap usage, provide liquidity to market, conduct airdrop events, reward early supporters and campaigns with exchanges and ecosystem partners. **Vesting schedule** * 100% unlocked at the Stable Mainnet launch #### Ecosystem & community - 40% of total token supply Supports long-term ecosystem and community growth: * Support the development of the Stable software and ecosystem * Developer grants * User onboarding incentives * Payment partner integrations * On-chain activity rewards * Hackathons, ambassador programs * Infrastructure grants **Vesting schedule** * **Initial unlock:** 8% of total supply unlocked at the Stable Mainnet launch. These tokens fund incentives for strategic launch partners, liquidity needs, and early ecosystem growth campaigns. * **Total vesting period:** 3-year linear vesting thereafter for the 32% of total supply #### Team - 25% of total token supply * Allocated to founding team members, engineers, researchers, and contributors * Designed to ensure long-term alignment between the team and the Stable ecosystem. **Vesting schedule** * **1-year cliff:** No tokens are unlocked in the first 12 months * **Total vesting period:** 48 months linear vesting from the Stable Mainnet launch #### Investors & advisors - 25% of total token supply * Allocated for fundraising rounds and advisory support. **Vesting schedule** * **1-year cliff:** No tokens are unlocked in the first 12 months * **Total vesting period:** 48 months linear vesting from the Stable Mainnet launch *** ### Emissions chart STABLE Token Emissions Chart STABLE Token Emissions Chart *** ### Economic design principles Stable's token economics were designed around three foundational goals: #### 1. Power a payments-optimized Layer 1 The STABLE token incentivizes high-throughput, low-latency infrastructure, supporting sub-second block confirmations and enterprise settlement guarantees. #### 2. Support sustainable ecosystem growth 40% of total token supply is dedicated to long-term growth, focusing on key development and growth areas. * Developer grants * Partner integrations * New ecosystem applications #### 3. Align long-term contributors via vesting The team allocation uses a 4-year linear vesting model, with a 1-year cliff, ensuring long-term alignment and continued contributions toward network development. *** ### Utility of the STABLE token The STABLE Token is an ERC-20 governance token on the Stable Mainnet. It can be used for: * Electing validators * Voting on protocol upgrades * Handling governance proposals * Serving as a credential to receive gas fee distribution from validators On the Stable network, all transactions use USDT0 as the native gas token. These USDT0 gas fees are collected into a treasury managed by smart contracts. When token holders stake their STABLE tokens to validators, validators may choose to distribute gas fees from the treasury proportionally to stakers. ## Wallets ### Wallets overview table | **Provider** | **Category** | **Security Method** | **Docs / Get Started** | **Notes** | | :----------------------------------------------------------------- | :---------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | | Stable Pay | User Wallet | TSS-MPC-based self-custody | [https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain](https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain) | USDT-native payment wallet built on Stable; optimized for instant transfers | | [Wallet Development Kit by Tether (WDK)](https://wallet.tether.io) | Wallet SDK | Self-custody | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether's open-source SDK for building self-custodial wallets across multi-chain | | [Binance Wallet](https://www.binance.com/en/web3wallet) | User Wallet | MPC-based self-custody / semi-custody wallet | [https://developers.binance.com/docs/binance-spot-api-docs/README](https://developers.binance.com/docs/binance-spot-api-docs/README) | Multi-chain wallet, supports Stable USDT | | [Reown](https://reown.com/) (formerly WalletConnect / WalletKit) | Connectivity / Wallet Infra | Protocol-level signing & secure relay | [https://docs.reown.com/appkit/overview](https://docs.reown.com/appkit/overview) | Supports 600+ wallets, multi-chain, SDK-based integration, ideal for embedded wallet flows | | [Bitget Wallet](https://web3.bitget.com/en) | User Wallet | Non-custodial wallet (private keys managed by user) | [https://web3.bitget.com/en/docs/](https://web3.bitget.com/en/docs/) | Built-in dApp browser; multi-asset & multi-chain support | | [Gate Wallet](https://www.gate.com/) (Gate Onchain) | User Wallet | Exchange-linked wallet | [https://www.gate.com/](https://www.gate.com/) | Exchange-linked wallet; suitable for CEX ↔ wallet flows | | [OKX Wallet](https://web3.okx.com/) (OKX Onchain) | User Wallet | Non-custodial / MPC for recovery | [https://www.okx.com/earn/onchain-earn](https://www.okx.com/earn/onchain-earn) | Multi-chain wallet with exchange integration | | [Anchorage](https://www.anchorage.com/) | Custodial / Institutional Wallet | Bank-grade regulated custody (federally chartered bank) | [https://www.anchorage.com/who-we-serve](https://www.anchorage.com/who-we-serve) | Institutional-grade custody for stable assets | | [Dynamic](https://www.dynamic.xyz/) | Embedded / In-App Wallet Infra | Managed-key / custody-infra via SDK or backend | [https://www.dynamic.xyz/docs/introduction/welcome](https://www.dynamic.xyz/docs/introduction/welcome) | Enables apps to embed wallet flows without external wallets | | [Alchemy](https://www.alchemy.com/) | Smart Wallets & Account Abstraction Infra | Bundler + Paymaster infrastructure (ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | Powers AA wallets; supports sponsored gas, smart accounts | | [Atomic Wallet](https://atomicwallet.io/) | User Wallet | Non-custodial (keys stored on user device) | [https://atomicwallet.io/assets-status](https://atomicwallet.io/assets-status) | Multi-asset mobile, desktop, and browser-extension wallet; buy USDT on Stable via the built-in [Simplex](/en/reference/ramps#simplex-by-nuvei) on-ramp | ### Category guide * **User Wallets:** These are traditional consumer-facing wallets such as mobile apps, browser extensions, or exchange-linked wallets. They allow users to hold USDT, make transfers, connect to dApps, and interact directly with Stable. * **Wallet SDK:** A software development kit that provides developers with prebuilt tools, APIs, and infrastructure to integrate wallet creation, key management, transaction signing, and blockchain interactions directly into their applications. * **Custodial / Institutional Wallets:** Platforms providing regulated, enterprise-grade asset custody for institutions. These solutions focus on compliance, governance controls, secure key management, and treasury operations rather than end-user flows. * **Embedded / In-App Wallets:** Wallets generated inside applications through SDKs or backend systems. These enable seamless onboarding for mainstream users without requiring them to install or understand external crypto wallets. * **Smart Wallets / Account Abstraction:** Programmable wallets that support custom logic such as gasless transactions, bundled operations, or automated execution. These extend basic wallet functionality with developer-defined behaviors. * **MPC Wallet Providers:** Key-management systems using multi-party computation (MPC) to distribute private key control across multiple parties or devices. Ideal for apps or enterprises needing high-security custody without traditional seed phrases. * **Connectivity Providers:** Protocols such as WalletConnect (Reown) that connect wallets and dApps. They don't store assets or act as wallets themselves; instead, they provide secure communication channels for transaction signing and interaction. ### 1. User wallets These are end-user wallets offered by major global exchanges. They allow users to hold USDT, transfer funds, and connect to applications on Stable. #### Stable Pay A non-custodial payment wallet built on Stable, designed for fast, stablecoin-native transactions. Stable Pay delivers instant USDT payments, predictable fees, and a simple user experience optimized for everyday transfers and commerce. **Capabilities** * Non-custodial wallet for Stable * Instant USDT payments * Predictable and consistent transaction costs * Built directly on Stable’s USDT-native settlement layer * Consumer-friendly UI designed for payments and commerce #### Binance Wallet A widely used multi-chain wallet integrated with the world’s largest exchange by volume. **Capabilities** * Supports Stable USDT * Direct integration with Binance ecosystem * Mobile and extension wallet options #### Bitget Wallet A multi-asset wallet connected to the Bitget exchange ecosystem, supporting crypto, stocks, and ETFs. **Capabilities** * Supports Stable USDT * Built-in dApp browser * Seamless integration with Bitget trading accounts #### Gate Wallet (Gate Onchain) A wallet product backed by one of the largest spot exchanges globally. **Capabilities** * Supports Stable USDT * Easy transfers between Gate exchange and wallet * dApp and web-app connectivity #### OKX Wallet (OKX Onchain) A powerful, multi-chain wallet used globally. **Capabilities** * Supports Stable USDT * Deep OKX ecosystem integration * Web, mobile, and extension wallet options #### Atomic Wallet A non-custodial multi-asset wallet available on desktop, mobile, and as a browser extension. Atomic Wallet integrates the [Simplex by Nuvei](/en/reference/ramps#simplex-by-nuvei) on-ramp, so users can buy USDT on Stable directly inside the app with cards, Apple Pay, Google Pay, or bank transfer. **Capabilities** * Holds, sends, and receives USDT on Stable * Private keys stored locally on the user device * In-app buy flow for USDT on Stable via Simplex * Available on Windows, macOS, Linux, iOS, Android, and Chrome **Get started**: Download Atomic Wallet from [atomicwallet.io](https://atomicwallet.io/downloads), add Stable as a network, and use the in-app Buy flow to purchase USDT on Stable through Simplex. ### 2. Wallet SDK #### Development Kit by Tether (WDK) An open-source SDK from Tether for building self-custodial wallets across any platform and blockchain. **Capabilities** * Multi-Chain Support: Bitcoin, Ethereum, TON, TRON, Solana, Spark, and more * Agentic Wallets: Native support for AI agent wallets and x402 payments on Stable * DeFi Integration: Plug-in support for swaps, bridges, and lending protocols * Extensible Design: Add custom modules for new blockchains or protocols **Get started**: Install [`@tetherto/wdk`](https://www.npmjs.com/package/@tetherto/wdk) and [`@tetherto/wdk-wallet-evm`](https://www.npmjs.com/package/@tetherto/wdk-wallet-evm), then follow the [WDK documentation](https://docs.wallet.tether.io) to configure Stable as your target chain. ### 3. Custodial & institutional wallets #### Anchorage A federally chartered national bank providing institutional-grade custody for digital assets. **Capabilities** * Secure custody for Stable USDT * Full compliance and regulatory oversight * Enterprise-grade key management and access controls ### 4. Embedded / in-app wallets Wallets embedded directly into applications via SDKs, enabling seamless user onboarding and payment flows. #### Dynamic Enterprise-grade wallet infrastructure serving thousands of applications and over 40M users. **Capabilities** * Wallet creation and authentication * Embedded wallet flows * User onboarding for apps and fintechs **Get started**: Follow the [Dynamic SDK setup docs](https://docs.dynamic.xyz/introduction/welcome) to install the SDK and configure Stable as a supported network in your app. #### Reown (formerly WalletConnect) A widely adopted standard for connecting wallets to applications. **Capabilities** * Secure wallet-to-dApp connections * Supports mobile, desktop, and extension wallets * Broad ecosystem compatibility **Reown SDK for wallet onboarding** Stable supports integrations with the **Reown SDK** to help developers deliver seamless wallet and onboarding experiences for users. Reown provides an open-source, all-in-one SDK that serves as the official gateway to the WalletConnect Network. It enables smooth wallet connections, transactions, logins, embedded wallets (email and social login), on-chain payments, in-app swaps, and more within your application. **Get Started** * Visit Reown's documentation: [https://docs.reown.com/overview](https://docs.reown.com/overview) ### 5. Smart wallets & account abstraction Infrastructure enabling programmable wallets, gasless transactions, spending rules, and advanced UX. #### Overview table | **Provider** | **Category** | **Security Method** | **Docs / Get Started** | **Notes** | | :------------------------------------------ | :------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------- | :------------------------------------ | | [**Holdstation**](https://holdstation.com/) | Smart Wallet (AA) | Smart contract wallet + biometric auth | [https://docs.holdstation.com/holdstation/](https://docs.holdstation.com/holdstation/) | Gasless flows, DeFi-native wallet | | [**Daimo**](https://pay.daimo.com/) | AA Payments Wallet | Smart account, no seed phrase | [https://paydocs.daimo.com/](https://paydocs.daimo.com/) | One-click payments, stablecoin-first | | [**Alchemy**](https://alchemy.com) | AA Infrastructure (Bundler/Paymaster) | Bundler + paymaster infra (ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | Enables AA wallets to build on Stable | #### Alchemy Alchemy provides the core AA infrastructure and APIs needed to deploy smart accounts, sponsor gas, and build consumer-grade wallets. **Capabilities** * Smart account SDK & APIs * Paymaster support for gasless actions * Gas abstraction tooling * Scalable infra for smart wallet developers **Get started**: Use the [Alchemy smart account SDK](https://docs.alchemy.com) to deploy ERC-4337 smart accounts and configure a paymaster for sponsored gas on Stable. **Docs**\ [https://docs.alchemy.com](https://docs.alchemy.com) #### Holdstation A smart contract wallet offering account abstraction and biometric-secured interactions.\ **Capabilities** * Full AA-enabled smart wallet * Gasless transactions and sponsored fees * Biometric authorization and session keys * Integrated trading and DeFi execution layer **Get started**: Explore the [Holdstation developer docs](https://docs.holdstation.com/holdstation/) to integrate smart wallet flows and sponsored transactions into your application. #### Daimo A consumer-grade, account-abstraction wallet designed for instant stablecoin spending and payments.\ **Capabilities** * AA-based UX with smart wallet execution * One-click payments across any chain * No seed phrases; secure key recovery * Ideal for payments apps and stablecoin utilities **Get started**: Visit the [Daimo Pay documentation](https://paydocs.daimo.com/) to add one-click stablecoin payment flows to your application on Stable. ### Stable network setup #### Adding Stable to your wallet **Network parameters :** * **Network Name:** Stable * **Chain ID:** 988 * **Currency:** USDT0 * **RPC URL:** [https://rpc.stable.xyz](https://rpc.stable.xyz) * **Block Explorer:** [https://stablescan.xyz](https://stablescan.xyz) #### Building wallet integrations You can add Stable support by: * Enabling signing and gas estimation via Stable RPC * Supporting USDT-native transfers * Integrating WalletConnect for dApps * Adding Stable to chain lists or metadata registries ### Have a wallet integrating Stable? You can reach the team at [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) to be listed in this section. ## Account abstraction with EIP-7702 This guide walks through applying EIP-7702 to an EOA and using the delegation for three patterns: batch payments, spending limits, and session keys. The EOA keeps its address and private key throughout. :::note **Concept:** For what EIP-7702 enables on Stable and the security considerations, see [EIP-7702](/en/explanation/eip-7702). ::: ### Prerequisites * Understanding of EOA vs. smart contract accounts (EOAs have no code by default). * Familiarity with EVM transaction types ([EIP-2718](https://eips.ethereum.org/EIPS/eip-2718)). ### Overview EIP-7702 introduces a new transaction type (`0x04`) that carries an `authorizationList`. Each authorization designates a smart contract whose code the EOA will execute for that transaction. The flow is: 1. **Choose or deploy a delegate contract**: a standard Solidity contract implementing the logic you want EOAs to use. You can use a deployed contract or deploy your own. Use an audited contract whenever possible. 2. **Sign an authorization**: the EOA owner signs a message authorizing the delegate contract. 3. **Submit an EIP-7702 transaction**: the transaction includes the authorization, and the EOA runs the delegate's code during execution. ### Use case: batch transactions The steps below walk through this flow using `Multicall3` as the delegate contract. `Multicall3` is a widely deployed utility contract that aggregates multiple calls into a single transaction. By designating `Multicall3` as the EIP-7702 delegate, an EOA can batch arbitrary contract interactions (token transfers, approvals, contract reads, or any combination) into one atomic transaction. Batch payments are one example: instead of sending ten separate transactions for a payroll run, the EOA executes them all at once. #### Step 1: Use Multicall3 as a delegate contract `Multicall3` is deployed at `0xcA11bde05977b3631167028862bE2a173976CA11` on Stable. Since it's already deployed and widely used, you don't need to deploy your own delegate. Signing an EIP-7702 authorization grants the delegate full execution authority over your EOA. ```solidity // Multicall3 interface (relevant functions only) interface IMulticall3 { struct Call3 { address target; bool allowFailure; bytes callData; } struct Result { bool success; bytes returnData; } /// @notice Aggregate calls, allowing each to succeed or fail independently function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); } ``` #### Step 2: Sign an authorization The EOA owner signs an authorization that designates the delegate contract. This authorization is included in the EIP-7702 transaction. ```javascript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_TESTNET_CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const DELEGATE_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); ``` ```javascript // signAuthorization.ts import { ethers } from "ethers"; import { DELEGATE_ADDRESS, STABLE_TESTNET_CHAIN_ID, provider, wallet } from "./config"; export async function signAuthorization() { const authorization = { chainId: STABLE_TESTNET_CHAIN_ID, address: DELEGATE_ADDRESS, nonce: await provider.getTransactionCount(wallet.address), }; return wallet.signAuthorization(authorization); } ``` #### Step 3: Submit an EIP-7702 transaction Combine the authorization with a call to `Multicall3.aggregate3`. This example batches three USDT0 transfers in a single transaction. ```javascript import { ethers } from "ethers"; import { wallet, USDT0_ADDRESS } from "./config"; import { signAuthorization } from "./signAuthorization"; const usdt0Interface = new ethers.Interface([ "function transfer(address to, uint256 amount)", ]); const batchInterface = new ethers.Interface([ "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) returns ((bool success, bytes returnData)[])", ]); async function main() { const recipients = [ { to: "0xAlice...", amount: ethers.parseUnits("100", 6) }, { to: "0xBob...", amount: ethers.parseUnits("200", 6) }, { to: "0xCarol...", amount: ethers.parseUnits("150", 6) }, ]; const batchData = batchInterface.encodeFunctionData("aggregate3", [ recipients.map(({ to, amount }) => ({ target: USDT0_ADDRESS, allowFailure: false, callData: usdt0Interface.encodeFunctionData("transfer", [to, amount]), })), ]); const signedAuth = await signAuthorization(); const tx = await wallet.sendTransaction({ type: 4, // EIP-7702 transaction type to: wallet.address, // call is directed at the EOA itself data: batchData, // aggregate3 call to execute authorizationList: [signedAuth], maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Batch transactions executed in tx:", receipt.hash); } ``` ```text Batch transactions executed in tx: 0x... ``` The EOA executes all three calls in a single atomic transaction via `Multicall3.aggregate3`. The delegation persists until explicitly changed or cleared. While this example shows batch payments, the same pattern works for any combination of contract calls. ### Use case: spending limits A delegate contract can enforce per-transaction or daily caps on an EOA without account migration. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title SpendingLimitExecutor /// @notice Delegate contract that enforces daily spending caps contract SpendingLimitExecutor { mapping(address => uint256) public dailyLimit; mapping(address => uint256) public spentToday; mapping(address => uint256) public lastResetDay; function setDailyLimit(uint256 limit) external { dailyLimit[msg.sender] = limit; } function executeWithLimit( address target, uint256 value, bytes calldata data ) external payable { uint256 today = block.timestamp / 1 days; if (today > lastResetDay[msg.sender]) { spentToday[msg.sender] = 0; lastResetDay[msg.sender] = today; } spentToday[msg.sender] += value; require( spentToday[msg.sender] <= dailyLimit[msg.sender], "daily limit exceeded" ); (bool success,) = target.call{value: value}(data); require(success, "call failed"); } } ``` ### Use case: session keys Session keys allow a dApp to execute transactions on behalf of an EOA within scoped permissions: a time window and an allowed set of target contracts. This is useful for dApps where frequent on-chain interactions would otherwise require repeated wallet approvals. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title SessionKeyExecutor /// @notice Delegate contract that grants scoped, time-limited access to a session key contract SessionKeyExecutor { struct Session { address key; uint256 validUntil; uint256 spendingLimit; uint256 spent; } mapping(address => Session) public sessions; mapping(address => mapping(address => bool)) public allowedTargets; /// @notice Register a session key with scoped permissions function startSession( address key, uint256 validUntil, uint256 spendingLimit, address[] calldata targets ) external { sessions[msg.sender] = Session({ key: key, validUntil: validUntil, spendingLimit: spendingLimit, spent: 0 }); for (uint256 i = 0; i < targets.length; i++) { allowedTargets[msg.sender][targets[i]] = true; } } /// @notice Execute a call using the session key function executeAsSessionKey( address owner, address target, uint256 value, bytes calldata data ) external { Session storage session = sessions[owner]; require(msg.sender == session.key, "not session key"); require(block.timestamp <= session.validUntil, "session expired"); require(allowedTargets[owner][target], "target not allowed"); uint256 beforeBalance = owner.balance; (bool success,) = target.call{value: value}(data); require(success, "call failed"); session.spent += owner.balance - beforeBalance; require(session.spent <= session.spendingLimit, "budget exceeded"); } /// @notice Revoke the active session function revokeSession() external { delete sessions[msg.sender]; } } ``` ### Important considerations * **Persistent delegation**: the delegation persists until the EOA explicitly changes or clears it. It is not limited to a single transaction. * **Gas costs**: EIP-7702 transactions have slightly higher base gas due to authorization processing, offset when the delegate batches multiple calls. * **Use audited delegates**: a malicious delegate contract can drain the EOA's assets. Only delegate to contracts that have been audited. ### Key takeaways * EIP-7702 lets EOAs execute smart contract logic without migrating to a new account type. * On Stable, EIP-7702 enables batch payments, spending limits, and scoped session keys on existing EOAs. * The delegation persists until explicitly changed. Always use an audited delegate contract. ### Next recommended * [**Subscribe and collect**](/en/how-to/subscribe-and-collect) — Apply EIP-7702 to recurring subscription payments with a SubscriptionManager. * [**EIP-7702 concept**](/en/explanation/eip-7702) — Understand the delegation model before you ship it. * [**EIP-7702 reference**](/en/reference/eip-7702-api) — Look up the `0x04` transaction format and authorization fields. ## Build an MPP endpoint on Stable This guide walks through writing a custom [MPP](/en/explanation/mpp) payment method for USDT0 on Stable and serving an MPP-gated endpoint. The buyer signs an [ERC-3009](/en/explanation/erc-3009) `transferWithAuthorization`, the server validates it through `mppx`'s `verify()` hook, and settlement happens in a separate step you control. :::note **Concept:** For what MPP is and how it relates to x402, see [Machine Payments Protocol (MPP)](/en/explanation/mpp). For the x402 equivalent, see [Build a pay-per-call API](/en/how-to/build-pay-per-call). ::: :::note The example uses Stable mainnet. Use small amounts when testing. ::: ### What you'll build An HTTP endpoint that returns `402 Payment Required` with an MPP `WWW-Authenticate` challenge, accepts a signed credential in the `Authorization` header, verifies it, settles `transferWithAuthorization` on USDT0, and returns the response with a `Payment-Receipt` header. ```text step 1. Client: GET /weather (no Authorization header) Server: 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[...usdt0-stable charge for $0.001...]" step 2. Client signs an ERC-3009 authorization with their viem account step 3. Client: GET /weather + Authorization header containing the serialized credential Server: verify() validates the EIP-712 signature Server: settle() submits transferWithAuthorization on Stable (~700ms block confirmation) Server: 200 OK { weather: "sunny" } Payment-Receipt: reference="0x8f3a...", status="success" step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a... ``` ### Prerequisites * A funded USDT0 wallet on Stable. See [Use the faucet](/en/how-to/use-faucet) or [Move USDT0](/en/tutorial/send-usdt0). * Node 20+ with `mppx`, `viem`, and `zod` installed. * A seller account (an EOA) on Stable. For the default settlement path, the seller pays gas in USDT0; the [Gas Waiver](#alternative-settle-through-the-gas-waiver) section shows the zero-gas variant. ```bash npm install mppx viem zod express ``` ### 1. Define the shared schema `Method.from()` declares the intent and the schemas for the request (Challenge) and the credential payload. Both client and server import this definition. ```typescript // src/method.ts import { Method } from "mppx"; import { z } from "zod"; import { parseUnits } from "viem"; export const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; export const CHAIN_ID = 988; // Request: the Challenge payload the server sends to the client. const zRequest = z.pipe( z.object({ chainId: z.literal(CHAIN_ID), asset: z.literal(USDT0_STABLE), amount: z.string(), // human-readable, e.g. "0.001" decimals: z.literal(6), payTo: z.string().regex(/^0x[a-fA-F0-9]{40}$/), validAfter: z.number().int().nonnegative(), validBefore: z.number().int().positive(), nonce: z.string().regex(/^0x[a-fA-F0-9]{64}$/), }), z.transform(({ amount, decimals, ...rest }) => ({ ...rest, amount: parseUnits(amount, decimals).toString(), // atomic units })), ); // Credential payload: what the client returns after signing. const zPayload = z.object({ from: z.string().regex(/^0x[a-fA-F0-9]{40}$/), signature: z.string().regex(/^0x[a-fA-F0-9]{130}$/), // 65-byte hex }); export const usdt0Stable = Method.from({ intent: "charge", name: "usdt0-stable", schema: { request: zRequest, credential: { payload: zPayload } }, }); // EIP-712 domain + type, used by both client and server. export const EIP712_DOMAIN = { name: "USDT0", version: "1", chainId: CHAIN_ID, verifyingContract: USDT0_STABLE, } as const; export const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], } as const; ``` ```text usdt0Stable.name === "usdt0-stable" usdt0Stable.intent === "charge" ``` ### 2. Server: verify the credential `Method.toServer` wires `verify()` into `mppx`. The function receives the deserialized credential (challenge + payload) and must throw on invalid proofs or return a `Receipt`. ```typescript // src/server-method.ts import { Method, Receipt } from "mppx"; import { verifyTypedData } from "viem"; import { usdt0Stable, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPES, } from "./method"; export const usdt0StableServer = Method.toServer(usdt0Stable, { async verify({ credential }) { const { request } = credential.challenge; const { from, signature } = credential.payload; const valid = await verifyTypedData({ address: from as `0x${string}`, domain: EIP712_DOMAIN, types: TRANSFER_WITH_AUTHORIZATION_TYPES, primaryType: "TransferWithAuthorization", message: { from: from as `0x${string}`, to: request.payTo as `0x${string}`, value: BigInt(request.amount), validAfter: BigInt(request.validAfter), validBefore: BigInt(request.validBefore), nonce: request.nonce as `0x${string}`, }, signature: signature as `0x${string}`, }); if (!valid) throw new Error("Invalid ERC-3009 signature"); // The Receipt's reference is filled in with the tx hash after settle(). return Receipt.from({ method: usdt0Stable.name, reference: "pending", status: "success", timestamp: new Date().toISOString(), }); }, }); ``` ```text { method: "usdt0-stable", reference: "pending", status: "success", timestamp: "2026-06-01T12:34:56.000Z" } ``` :::warning `verify()` checks the signature but does not check nonce uniqueness or whether the authorization has already been spent. The chain enforces both at submission time: `transferWithAuthorization` reverts on a used nonce. The settle step turns those reverts into errors the server can surface to the client. ::: ### 3. Settle: submit `transferWithAuthorization` Settlement is intentionally separate from `verify()`. After `verify()` returns, you submit the authorization on-chain through whichever path fits your operational model. Three options, in order of recommendation. #### Default: server submits directly The seller's EOA submits `transferWithAuthorization` to USDT0 with the signed authorization. The seller pays gas in USDT0 (Stable's native gas token), so there is no separate gas-token balance to manage. ```typescript // src/settle.ts import { createWalletClient, http, parseSignature } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { stable } from "viem/chains"; import { USDT0_STABLE } from "./method"; const USDT0_ABI = [ { name: "transferWithAuthorization", type: "function", stateMutability: "nonpayable", inputs: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, { name: "v", type: "uint8" }, { name: "r", type: "bytes32" }, { name: "s", type: "bytes32" }, ], outputs: [], }, ] as const; const seller = privateKeyToAccount(process.env.SELLER_KEY as `0x${string}`); const wallet = createWalletClient({ account: seller, chain: stable, transport: http("https://rpc.stable.xyz"), }); export async function settleDirect(credential: { challenge: { request: any }; payload: { from: string; signature: string }; }): Promise<{ txHash: `0x${string}` }> { const { request } = credential.challenge; const { v, r, s } = parseSignature(credential.payload.signature as `0x${string}`); const txHash = await wallet.writeContract({ address: USDT0_STABLE, abi: USDT0_ABI, functionName: "transferWithAuthorization", args: [ credential.payload.from as `0x${string}`, request.payTo as `0x${string}`, BigInt(request.amount), BigInt(request.validAfter), BigInt(request.validBefore), request.nonce as `0x${string}`, Number(v), r as `0x${string}`, s as `0x${string}`, ], }); return { txHash }; } ``` ```text { txHash: "0x8f3a1b2c..." } ``` #### Alternative: settle through the Gas Waiver Use Stable's [Gas Waiver](/en/how-to/integrate-gas-waiver) to submit the inner transaction at `gasPrice = 0`. The seller still signs the wrapping transaction, but pays no gas. Requires a Waiver Server API key. ```typescript // src/settle-waiver.ts import { encodeFunctionData } from "viem"; import { USDT0_STABLE } from "./method"; import { USDT0_ABI } from "./settle"; const WAIVER_SERVER = "https://waiver.stable.xyz"; // mainnet endpoint export async function settleViaWaiver( credential: { challenge: { request: any }; payload: { from: string; signature: string } }, signedInnerTxHex: `0x${string}`, ): Promise<{ txHash: `0x${string}` }> { const res = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.WAIVER_API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex] }), }); const lines = (await res.text()).trim().split("\n"); const result = JSON.parse(lines[0]); if (!result.success) throw new Error(`Settle failed: ${result.error?.message}`); return { txHash: result.txHash }; } ``` ```text { txHash: "0x8f3a1b2c..." } ``` See [Gas waiver protocol](/en/reference/gas-waiver-api) for how to build the signed inner transaction (`gasPrice: 0`, encoded `transferWithAuthorization` call) before posting. #### Alternative: hand off to an x402 facilitator If you already operate an x402 facilitator integration ([Semantic Pay](https://docs.semanticpay.io) or [Heurist](https://docs.heurist.ai/x402-products/facilitator)), you can reuse it as a settlement target. POST a `paymentPayload` to `/settle`; the facilitator submits the on-chain call. The exact `paymentPayload` shape is x402-middleware-internal and not specified at the wire level. The simplest path is to use the facilitator's own SDK to build the payload, or stick with the direct-submission path above. The facilitator does not need to speak MPP; it sees only the `transferWithAuthorization` fields. ### 4. Client: sign a credential `Method.toClient` wires `createCredential()` into `mppx`. The client reads the Challenge, signs the EIP-712 authorization with the agent's viem account, and serializes the credential. ```typescript // src/client-method.ts import { Credential, Method } from "mppx"; import { hexToSignature, parseSignature } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { usdt0Stable, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPES, } from "./method"; export function createUsdt0StableClient(privateKey: `0x${string}`) { const account = privateKeyToAccount(privateKey); return Method.toClient(usdt0Stable, { async createCredential({ challenge }) { const { request } = challenge; const signature = await account.signTypedData({ domain: EIP712_DOMAIN, types: TRANSFER_WITH_AUTHORIZATION_TYPES, primaryType: "TransferWithAuthorization", message: { from: account.address, to: request.payTo as `0x${string}`, value: BigInt(request.amount), validAfter: BigInt(request.validAfter), validBefore: BigInt(request.validBefore), nonce: request.nonce as `0x${string}`, }, }); return Credential.serialize({ challenge, payload: { from: account.address, signature }, }); }, }); } ``` ```text "eyJjaGFsbGVuZ2UiOnsi..." // base64-serialized credential, ~600 bytes ``` ### 5. Wire the server together Use `mppx`'s Express middleware to issue Challenges, parse incoming `Authorization` headers, run `verify()`, call your settle function, and emit the `Payment-Receipt` header. ```typescript // src/server.ts import express from "express"; import { Mppx } from "mppx/express"; import { randomBytes } from "node:crypto"; import { usdt0StableServer } from "./server-method"; import { settleDirect } from "./settle"; const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`; const PORT = Number(process.env.PORT ?? 4022); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY!, methods: [usdt0StableServer], onVerified: async ({ credential, receipt }) => { const { txHash } = await settleDirect(credential); return { ...receipt, reference: txHash }; }, }); const app = express(); app.get( "/weather", mppx.charge({ amount: "0.001", method: "usdt0-stable", request: { chainId: 988, asset: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", decimals: 6, payTo: PAY_TO, validAfter: 0, validBefore: Math.floor(Date.now() / 1000) + 300, nonce: `0x${randomBytes(32).toString("hex")}`, }, })((_req, res) => { res.json({ weather: "sunny", temperature: 70 }); }), ); app.listen(PORT, () => { console.log(`MPP server listening on http://localhost:${PORT}`); }); ``` ```text MPP server listening on http://localhost:4022 ``` ### 6. Run the flow end to end Start the server, confirm the Challenge, run a client, and confirm settlement. :::warning The next step settles a real USDT0 payment on Stable mainnet. Use a dedicated wallet and small amounts. ::: #### Confirm the Challenge ```bash curl -i http://localhost:4022/weather ``` ```text HTTP/1.1 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[{\"method\":\"usdt0-stable\",\"request\":{...}}]" Content-Type: application/json {"error":"Payment required"} ``` #### Send a paid request ```typescript // src/client.ts import { Mppx } from "mppx/client"; import { createUsdt0StableClient } from "./client-method"; const client = Mppx.create({ methods: [createUsdt0StableClient(process.env.BUYER_KEY as `0x${string}`)], }); const res = await fetch("http://localhost:4022/weather", { // mppx wraps fetch with the 402 retry loop: ...client.fetchOptions(), }); console.log(res.status, await res.json()); console.log("Payment-Receipt:", res.headers.get("Payment-Receipt")); ``` ```bash npx tsx src/client.ts ``` ```text 200 { weather: "sunny", temperature: 70 } Payment-Receipt: reference="0x8f3a1b2c...", status="success", timestamp="2026-06-01T12:34:56.000Z" ``` #### Verify on Stablescan Open `https://stablescan.xyz/tx/0x8f3a1b2c...` and confirm the `transferWithAuthorization` settled to your `PAY_TO` address. ### What you just did * Paid in USDT0, denominated in dollars, with no gas-token balance to manage on the buyer side. * Used MPP's `WWW-Authenticate` / `Authorization` / `Payment-Receipt` wire format on the client-server hop. * Settled with `transferWithAuthorization` on Stable in the same HTTP request lifecycle (\~700 ms block time). ### Next recommended * [**MPP concept**](/en/explanation/mpp) — Read how MPP relates to x402 and what the other intents look like. * [**MPP sessions**](/en/explanation/mpp-sessions) — Stream micropayments with off-chain vouchers when per-request settlement is too expensive. * [**Facilitators**](/en/reference/agentic-facilitators) — Use Semantic Pay or Heurist as the settlement target instead of submitting directly. ## Learn P2P payments This guide walks through building a P2P payment application on Stable. The app handles the full payment lifecycle: the sender transfers USDT0 directly, the receiver detects the incoming payment in real time, and both can query their own transaction history. Same architecture as any wallet or payment interface, whether a mobile app, web checkout, or backend service. No middleware, no intermediary. For the conceptual overview, see [P2P payments](/en/reference/p2p-payments). To skip the ABI work and reach a working `transfer` in a few lines, use the [Stable SDK](/en/explanation/sdk-overview). ### What you'll build Five scripts forming a minimal payment app: * `wallet.ts` — create or restore a wallet. * `getBalance.ts` — query the current USDT0 balance. * `send.ts` — send USDT0 to another address. * `receive.ts` — watch for incoming payments in real time. * `history.ts` — query past Transfer events for an address. #### Demo ```text step 1. Alice creates wallet → address: 0xAlice... step 2. Alice's balance: 0.01 USDT0 step 3. Alice sends 0.001 USDT0 to Bob tx: 0x8f3a...2d41 gas fee: 0.000021 USDT0 Alice balance: 0.008979 USDT0 step 4. Bob receives payment (real-time event) from: 0xAlice... amount: 0.001 USDT0 tx: 0x8f3a...2d41 ``` ### Prerequisites * Node.js 20 or later. * A private key with testnet USDT0 (see [Quick start](/en/tutorial/quick-start) to fund a wallet). ### Project setup ```bash mkdir stable-p2p && cd stable-p2p npm init -y && npm install ethers dotenv ``` ```text added 2 packages, audited 3 packages in 1s ``` Create `config.ts` shared by every script: ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_WS = "wss://rpc.testnet.stable.xyz"; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_RPC); ``` ### 1. Create or restore a wallet A wallet is a key pair derived from a seed phrase. Generate one for a new user and return the phrase so they can back it up. A returning user restores their wallet from the same phrase. ```typescript // wallet.ts import { ethers } from "ethers"; import { provider } from "./config"; /** Create a new wallet for a new user. */ export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // display to user for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address }; } if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase); } ``` ```bash npx tsx wallet.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` ### 2. Check the balance USDT0 is the native asset on Stable, so balance queries work exactly like ETH on Ethereum. Native balance is 18 decimals, use `formatEther` for display. ```typescript // getBalance.ts import { ethers } from "ethers"; import { provider } from "./config"; export async function getBalance(address: string) { const balance = await provider.getBalance(address); return ethers.formatEther(balance); // 18 decimals } if (import.meta.url === `file://${process.argv[1]}`) { const address = process.argv[2]; const balance = await getBalance(address); console.log("Balance:", balance, "USDT0"); } ``` ```bash npx tsx getBalance.ts 0xAlice...1234 ``` ```text Balance: 0.01 USDT0 ``` ### 3. Send a payment The sender signs and submits a transfer directly. On Stable, USDT0 is the native asset, so a simple value transfer is the cheapest path (21,000 gas). This is the same code path as "Send" in any payment app. ```typescript // send.ts import { ethers } from "ethers"; import { provider } from "./config"; export async function sendPayment( senderKey: string, recipient: string, amount: string // e.g. "0.001" for 0.001 USDT0 ) { const wallet = new ethers.Wallet(senderKey, provider); const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: ethers.parseEther(amount), maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); console.log("Payment sent:", tx.hash); const receipt = await tx.wait(1); if (receipt!.status === 1) console.log("Payment settled"); return tx.hash; } if (import.meta.url === `file://${process.argv[1]}`) { const [, , recipient, amount] = process.argv; await sendPayment(process.env.PRIVATE_KEY!, recipient, amount); } ``` ```bash npx tsx send.ts 0xBob...5678 0.001 ``` ```text Payment sent: 0x8f3a...2d41 Payment settled ``` ### 4. Receive payments in real time The receiver listens for incoming `Transfer` events. This is equivalent to push notifications in a traditional payment app. On Stable, single-slot finality means the receiver sees a payment almost instantly. ```typescript // receive.ts import { ethers } from "ethers"; import { STABLE_WS, USDT0_ADDRESS } from "./config"; const wsProvider = new ethers.WebSocketProvider(STABLE_WS); const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], wsProvider ); export function watchIncomingPayments(address: string) { const filter = usdt0.filters.Transfer(null, address); usdt0.on(filter, (from: string, to: string, value: bigint, event: any) => { console.log("Payment received:"); console.log(" from: ", from); console.log(" amount:", ethers.formatUnits(value, 6), "USDT0"); console.log(" tx: ", event.log.transactionHash); }); console.log("Watching for incoming payments to", address); } if (import.meta.url === `file://${process.argv[1]}`) { watchIncomingPayments(process.argv[2]); } ``` ```bash npx tsx receive.ts 0xBob...5678 ``` ```text Watching for incoming payments to 0xBob...5678 Payment received: from: 0xAlice...1234 amount: 0.001 USDT0 tx: 0x8f3a...2d41 ``` :::note Native transfers (value transfers) also emit a `Transfer` event on the USDT0 ERC-20 contract because USDT0 is both the native asset and an ERC-20 token on Stable. A single event listener covers both transfer methods. ::: ### 5. Query transaction history Query past `Transfer` events to build a transaction history view, like a bank statement or transaction list in any payment app. ```typescript // history.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], provider ); export async function getTransactionHistory(address: string, fromBlock?: number) { if (fromBlock === undefined) { const latest = await provider.getBlockNumber(); fromBlock = Math.max(0, latest - 10_000); } const [sentEvents, receivedEvents] = await Promise.all([ usdt0.queryFilter(usdt0.filters.Transfer(address, null), fromBlock), usdt0.queryFilter(usdt0.filters.Transfer(null, address), fromBlock), ]); return [ ...sentEvents.map((e: any) => ({ type: "sent" as const, counterparty: e.args[1], amount: ethers.formatUnits(e.args[2], 6), txHash: e.transactionHash, block: e.blockNumber, })), ...receivedEvents.map((e: any) => ({ type: "received" as const, counterparty: e.args[0], amount: ethers.formatUnits(e.args[2], 6), txHash: e.transactionHash, block: e.blockNumber, })), ].sort((a, b) => b.block - a.block); } if (import.meta.url === `file://${process.argv[1]}`) { const history = await getTransactionHistory(process.argv[2]); for (const tx of history) { console.log(`${tx.type} ${tx.amount} USDT0 ${tx.counterparty} ${tx.txHash}`); } } ``` ```bash npx tsx history.ts 0xAlice...1234 ``` ```text sent 0.001 USDT0 0xBob...5678 0x8f3a...2d41 received 0.01 USDT0 0xFaucet... 0x22b1...3f09 ``` :::warning Scanning wide block ranges (millions of blocks) can time out and exceed RPC rate limits. For production, use the [Stablescan Etherscan-compatible API](https://stablescan.xyz) for paginated history queries — every transaction is already indexed. ::: ### Next recommended * [**Subscribe and collect**](/en/how-to/subscribe-and-collect) — Pull-based recurring subscriptions with EIP-7702 delegation. * [**Paying with invoice**](/en/how-to/pay-with-invoice) — Settle invoices with ERC-3009 and deterministic nonces. * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Reference the basic native vs. ERC-20 transfer flow. ## Build a pay-per-call API This guide walks through monetizing an API endpoint with x402. The server adds a payment handler, the client pays per request, and settlement happens within the HTTP lifecycle. :::note **Concept:** For the x402 protocol and why it fits Stable, see [x402](/en/explanation/x402). For the high-level use case model, see [Pay-per-call APIs](/en/reference/pay-per-call). ::: :::note The Semantic facilitator currently operates on mainnet only. The examples in this guide use Stable mainnet. Use small amounts when testing. ::: ### What you'll build A paid HTTP API where the server responds with `402 Payment Required`, the client pays per request, and the facilitator settles USDT0 on-chain within the HTTP lifecycle. #### Demo ```text step 1. Client: GET /weather (no payment) Server: 402 Payment Required PAYMENT-REQUIRED: { amount: "1000", asset: USDT0, network: eip155:988 } step 2. Client signs ERC-3009 authorization step 3. Client: GET /weather + PAYMENT-SIGNATURE header Server: forwards to facilitator → transferWithAuthorization settles on-chain (~700ms block confirmation) Server: 200 OK { weather: "sunny", temperature: 70 } PAYMENT-SETTLE-RESPONSE: { txHash: "0x8f3a...", paid: "0.001 USDT0" } step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a... ``` ### Overview **Seller (server):** ```typescript // --- Server --- app.use(paymentMiddleware({ "GET /weather": { price: { amount: "1000", asset: USDT0 }, payTo: sellerAddress, }, "POST /inference": { price: { amount: "50000", asset: USDT0 }, payTo: sellerAddress, }, }, resourceServer)); // Routes not listed in the config are not gated. ``` **Buyer (client):** ```typescript // --- Client --- account = new WalletAccountEvm(seedPhrase, { provider: RPC }); client = new x402Client(); fetchWithPayment = wrapFetchWithPayment(fetch, client); weatherResponse = fetchWithPayment("https://api.example.com/weather"); inferenceResponse = fetchWithPayment("https://api.example.com/inference", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: "Hello" }), }); // For each paid request: // 1. Initial request returns 402 with PAYMENT-REQUIRED header // 2. Client signs ERC-3009 authorization with wallet // 3. Client retries with PAYMENT-SIGNATURE header // 4. Facilitator settles on-chain, server returns the response ``` ### Seller: set up paid endpoints The seller adds x402 middleware to define which routes require payment. When a request arrives without payment, the middleware responds with `402 Payment Required` and the payment terms. When a valid payment header is present, the middleware forwards it to a facilitator that verifies the signature and settles the payment on-chain. The seller only configures the price and the receiving address; the facilitator handles verification and settlement. ```bash npm install express @x402/express @x402/evm @x402/core ``` #### Pricing Each route specifies the payment amount in USDT0 base units (6 decimals), the network, and the address to receive funds. For example, `"1000"` equals `$0.001` and `"50000"` equals `$0.05`. ```typescript price: { amount: "1000", // base units (6 decimals) asset: USDT0_STABLE, // USDT0 contract address extra: { name: "USDT0", version: "1", decimals: 6 }, // EIP-712 domain info } ``` The `extra` fields (`name`, `version`, `decimals`) are used by the buyer's client for EIP-712 signature construction and must match the on-chain USDT0 contract. #### Route configuration Routes are mapped using the `METHOD /path` format. Each route specifies the accepted payment scheme, network, price, and the address to receive funds (`payTo`). The `description` and `mimeType` fields help buyers and AI agents discover what the endpoint provides. Routes not listed in the config are not gated and behave like normal Express routes. ```typescript // server.ts import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`; const FACILITATOR_URL = "https://x402.semanticpay.io/"; const STABLE_NETWORK = "eip155:988"; // Stable Mainnet CAIP-2 ID const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL }); const resourceServer = new x402ResourceServer(facilitatorClient) .register(STABLE_NETWORK, new ExactEvmScheme()); const app = express(); app.use( paymentMiddleware( { // Example 1: Configure a paid GET route "GET /weather": { accepts: [ { scheme: "exact", network: STABLE_NETWORK, price: { amount: "1000", // $0.001 asset: USDT0_STABLE, extra: { name: "USDT0", version: "1", decimals: 6 }, }, payTo: PAY_TO, }, ], description: "Weather data", mimeType: "application/json", }, // Example 2: Configure a paid POST route "POST /inference": { accepts: [ { scheme: "exact", network: STABLE_NETWORK, price: { amount: "50000", // $0.05 asset: USDT0_STABLE, extra: { name: "USDT0", version: "1", decimals: 6 }, }, payTo: PAY_TO, }, ], description: "AI inference endpoint", mimeType: "application/json", }, }, resourceServer, ), ); app.get("/weather", (req, res) => { res.json({ weather: "sunny", temperature: 70 }); }); app.post("/inference", (req, res) => { const { prompt } = req.body; res.json({ result: `Inference result for: ${prompt}` }); }); // Not listed in the config, so no payment required. app.get("/health", (req, res) => { res.json({ status: "ok", payTo: PAY_TO }); }); const PORT = process.env.PORT || 4021; app.listen(PORT, () => { console.log(`Server listening at http://localhost:${PORT}`); console.log(`GET /health - free`); console.log(`GET /weather - $0.001 per request`); console.log(`POST /inference - $0.05 per request`); }); ``` :::note x402 also provides middleware for Hono (`@x402/hono`) and Next.js (`@x402/next`). The pattern is the same: create a facilitator client, register the EVM scheme, and apply middleware. ::: ### Buyer: make paid requests The buyer accesses paid endpoints without going through manual payment flows. The buyer does not pay gas. The facilitator settles on-chain, and the buyer only pays the exact amount specified in the payment requirements. ```bash npm install @x402/fetch @x402/evm @tetherto/wdk-wallet-evm ``` #### Create a wallet and check balance ```typescript // client.ts import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, { provider: "https://rpc.stable.xyz", }).getAccount(0); console.log("Buyer address:", account.address); // USDT0 uses 6 decimals. A balance of 1000000 equals 1.00 USDT0. const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const balance = await account.getTokenBalance(USDT0_STABLE); console.log("USDT0 balance:", Number(balance) / 1e6, "USDT0"); ``` #### Connect to x402 and make a paid request `WalletAccountEvm` satisfies the signer interface that x402 expects, so it can be registered directly as the signer for the x402 client. Once registered, requests sent through the x402-enabled client handle 402 payment flows automatically. ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; const client = new x402Client(); registerExactEvmScheme(client, { signer: account }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("http://localhost:4021/weather"); const data = await response.json(); console.log("Response:", data); ``` Under the hood, `fetchWithPayment` intercepts the 402 response, parses the payment requirements (amount, token, network, recipient), signs an ERC-3009 `transferWithAuthorization` with the WDK wallet, and retries the request with the `PAYMENT-SIGNATURE` header. :::note If you prefer Axios, use `@x402/axios` with `wrapAxiosWithPayment` for the same automatic payment handling. ::: ### Test the payment flow Start the server and verify both the paid and free routes. :::warning This test flow runs on Stable mainnet. Each successful paid request settles a real USDT0 payment through the hosted facilitator. Use a dedicated wallet and small amounts only. ::: #### 1. Confirm the 402 response ```bash curl -i http://localhost:4021/weather ``` The response should be `402 Payment Required` with a `PAYMENT-REQUIRED` header containing the price, asset, and network. #### 2. Run the client ```bash npx tsx client.ts ``` The client handles the full cycle: receives the 402, signs the authorization, retries with payment, and prints the response. #### 3. Read the receipt After a successful paid request, the buyer can read the `PAYMENT-SETTLE-RESPONSE` header from the server response and parse the settlement receipt. ```typescript // (continued) client.ts import { x402HTTPClient } from "@x402/fetch"; const httpClient = new x402HTTPClient(client); const receipt = httpClient.getPaymentSettleResponse( (name) => response.headers.get(name), ); console.log("Payment receipt:", JSON.stringify(receipt, null, 2)); ``` ### Test without the live facilitator Because the Semantic facilitator is mainnet-only, you can't point your server at a testnet facilitator today. To iterate on server logic, route handlers, and middleware behavior without settling real payments, stub the facilitator client. ```typescript // server.test.ts import { x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; // Stub facilitator: accepts any signature, returns a fake settlement. const stubFacilitatorClient = { verify: async () => ({ isValid: true, payer: "0xMockPayer" }), settle: async () => ({ success: true, txHash: "0xMOCK000000000000000000000000000000000000000000000000000000000001", networkId: "eip155:988", }), }; export const testResourceServer = new x402ResourceServer(stubFacilitatorClient as any) .register("eip155:988", new ExactEvmScheme()); ``` Run unit tests against the stub to validate: * 402 responses include the correct `PAYMENT-REQUIRED` payload. * Requests with a valid `PAYMENT-SIGNATURE` header reach the handler. * Requests with a missing or malformed header get rejected before the handler runs. When you're ready to exercise real settlement, swap back to `HTTPFacilitatorClient` and run on mainnet with small amounts. :::warning Stubbed settlement only verifies middleware behavior. It doesn't prove your route handler is idempotent under real network latency or concurrent payments. Always finish with a live mainnet test against small amounts before shipping. ::: ### Advanced: lifecycle hooks x402 provides hooks to intercept and customize payment processing at key points in the flow. For example, the server can run logic before verification (e.g., checking API keys or subscriber status) to bypass payment for authorized requests, and the client can enforce spending limits before signing. For the full hook reference and examples, see [x402 Lifecycle Hooks](https://x402.semanticpay.io/docs/hooks). ### Next recommended * [**x402 concept**](/en/explanation/x402) — Understand the protocol and where it fits. * [**ERC-3009**](/en/explanation/erc-3009) — Review the settlement standard x402 uses. * [**Paying with MCP server**](/en/how-to/pay-with-mcp) — Wrap this API as an MCP tool so AI clients can call it through prompts. ## Create a wallet A Stable wallet is an Ethereum-standard key pair. Any wallet library that produces an EVM account works on Stable without modification. This guide shows two paths: ethers.js for most applications, and Tether's [WDK (Wallet Development Kit)](https://github.com/tetherto/wdk) for integrations that want a turnkey self-custody layer for agents and payments. :::note No registration, no Stable-specific account setup. A wallet can immediately receive USDT0 from the [testnet faucet](/en/how-to/use-faucet) or a mainnet transfer. ::: ### Prerequisites * Node.js 20 or later. ### Option 1: ethers.js Install the library and generate a key pair. ```bash npm install ethers ``` ```typescript // wallet.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); /** Create a new wallet for a new user. */ export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // show to the user once for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address }; } if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase); } ``` ```bash npx tsx wallet.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` :::warning Never log or store the seed phrase in plain text in production. Encrypt it at rest, or use a secrets manager. `ethers.Wallet.createRandom` returns the phrase once per call — if you lose it, funds are unrecoverable. ::: ### Option 2: Tether WDK The WDK wraps key derivation, signing, and transaction submission into a single interface. It's the right choice when you want self-custody without re-implementing common account flows, and it integrates directly with [x402](/en/how-to/build-pay-per-call) for agent payments. ```bash npm install @tetherto/wdk @tetherto/wdk-wallet-evm ``` ```typescript // wallet-wdk.ts import WDK from "@tetherto/wdk"; import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; function initWdk(seedPhrase: string) { return new WDK(seedPhrase) .registerWallet("stable", WalletManagerEvm, { provider: "https://rpc.testnet.stable.xyz", }); } /** Create a new wallet for a new user. */ export async function createWallet() { const seedPhrase = WDK.getRandomSeedPhrase(); const wdk = initWdk(seedPhrase); const account = await wdk.getAccount("stable", 0); return { account, address: await account.getAddress(), seedPhrase, // show to the user once for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export async function restoreWallet(seedPhrase: string) { const wdk = initWdk(seedPhrase); const account = await wdk.getAccount("stable", 0); return { account, address: await account.getAddress() }; } ``` ```bash npx tsx wallet-wdk.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` ### Fund the wallet Before the wallet can transact, it needs USDT0 for gas. On testnet, request from the faucet: ```bash open https://faucet.stable.xyz ``` Paste the address and select the button to receive 1 testnet USDT0 (enough for thousands of native transfers). For mainnet, send USDT0 from any supported exchange or bridge; see [Bridging to Stable](/en/explanation/usdt0-bridging). ### Check the balance Native USDT0 uses 18 decimals. The native balance is the one gas is paid from. ```typescript // balance.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const balance = await provider.getBalance("0xYourAddress"); console.log("Balance:", ethers.formatEther(balance), "USDT0"); ``` ```bash npx tsx balance.ts ``` ```text Balance: 1.0 USDT0 ``` ### Next recommended * [**Delegate with EIP-7702**](/en/how-to/account-abstraction) — Add batch payments, spending limits, and session keys to this wallet. * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Native and ERC-20 transfers on the same balance. * [**Fund a testnet wallet**](/en/how-to/use-faucet) — Faucet and Sepolia bridge options for larger test balances. Stable provides MCP servers, agent skills, and plain-text documentation files so AI editors and coding agents can work with Stable directly. This page covers how to wire each piece into your workflow, a copy-pasteable context block for non-MCP AI tools, and starter prompts for common tasks. ### MCP servers Stable runs two MCP servers. **Docs MCP** searches this docs site for concepts, guides, code snippets, and contract references. **Runtime MCP** interacts with the Stable chain for balance queries, transaction simulation, and execution. Both servers can be added to any MCP-compatible client. #### Cursor Open your MCP configuration file and add: ```json { "mcpServers": { "stable-docs": { "url": "https://docs.stable.xyz/mcp" }, "stable-runtime": { "url": "https://runtime.stable.xyz/mcp" } } } ``` Restart Cursor. Verify by asking: "How do I send USDT0 on Stable?" #### Claude Code ```bash claude mcp add stable-docs https://docs.stable.xyz/mcp claude mcp add stable-runtime https://runtime.stable.xyz/mcp ``` Verify by asking: "Search Stable docs for Gas Waiver integration steps." ### Agent skills Agent skills are predefined workflows that combine Docs MCP and Runtime MCP. When you ask an AI to perform a task like "send 100 USDT0 to three addresses," the skill handles the full sequence: look up the relevant docs, resolve addresses and parameters, check balances, simulate the transaction, and execute after approval. Skills are available as a Claude Code plugin. #### Install ```bash claude plugin add stable-xyz/agent-skills ``` Or install from the Claude Code marketplace. For the full skill definitions and source, see the [agent-skills repository](https://github.com/stable-xyz/agent-skills). ### Plain-text docs For AI tools that do not support MCP, Stable documentation is available as static text files. | **File** | **URL** | **Content** | | :-------------- | :----------------------------------------------------------------------------- | :-------------------------------------- | | `llms.txt` | [https://docs.stable.xyz/llms.txt](https://docs.stable.xyz/llms.txt) | Page index with titles and descriptions | | `llms-full.txt` | [https://docs.stable.xyz/llms-full.txt](https://docs.stable.xyz/llms-full.txt) | Full documentation in a single file | These files are static snapshots. For the most current content, use Docs MCP. #### Cursor 1. Go to **Settings > Features > Docs**. 2. Select **Add** and enter `https://docs.stable.xyz/llms-full.txt`. 3. Reference in chat with `@Stable`. #### Other tools Download `llms-full.txt` and include it in your project context or system prompt. ### Stable context block Paste this at the top of any AI chat or system prompt. It gives the model everything it needs to generate correct Stable code on the first attempt. ```markdown # Stable chain context Stable is a Layer 1 where USDT0 is the native gas token. Fully EVM-compatible. All standard EVM tools (Hardhat, Foundry, ethers.js, viem) work unchanged once you adjust three gas fields (see Behavioral differences below). ## Network | Field | Mainnet | Testnet | | :-------------- | :--------------------------------------- | :----------------------------------------- | | Chain ID | 988 | 2201 | | RPC | https://rpc.stable.xyz | https://rpc.testnet.stable.xyz | | Explorer | https://stablescan.xyz | https://testnet.stablescan.xyz | | Currency symbol | USDT0 | USDT0 | ## USDT0 contract addresses - Mainnet: 0x779ded0c9e1022225f8e0630b35a9b54be713736 - Testnet: 0x78cf24370174180738c5b8e352b6d14c83a6c9a9 ## Behavioral differences from Ethereum 1. **Gas token is USDT0, not ETH.** The `value` field in native transfers carries USDT0. Fees are denominated in USDT0. 2. **`maxPriorityFeePerGas` is always 0.** No tip-based ordering. Set it explicitly to `0n` or validators will reject or ignore tip components. 3. **USDT0 has a dual role**: native asset (18 decimals) AND ERC-20 (6 decimals) on the same balance. `address(x).balance` reports 18-decimal wei; `USDT0.balanceOf(x)` reports 6-decimal units. Values may differ by up to 0.000001 USDT0 due to fractional reconciliation. Never mirror native balance in an internal variable; always query at payout time. 4. **Transfer events are emitted for native transfers too.** A single Transfer event listener on the USDT0 ERC-20 contract covers both transfer paths. 5. **Single-slot finality (~700ms).** Once a block is committed, it cannot be reorged. No need to wait multiple confirmations. 6. **Gas Waiver** lets applications cover gas: user signs with `gasPrice = 0`, a governance-registered waiver wraps and submits. Contracts must be on the waiver's AllowedTarget policy. 7. **EIP-7702** is supported for delegating an EOA to a contract (type-4 tx). 8. **Precompile addresses**: Bank `0x...1003`, Distribution `0x...0801`, Staking `0x...0800`, StableSystem `0x...9999`. ## Common mistakes to avoid - Copying Ethereum priority-fee constants (2 gwei tips, etc.) — has no effect on Stable and can be rejected by wallets. - Using `ethers.parseUnits(x, 18)` for ERC-20 USDT0 amounts. ERC-20 uses 6 decimals; native transfers use 18. - Mirroring native balance in a `uint256 deposited` variable — USDT0 allowance-based operations (transferFrom, permit) can reduce a contract's native balance without invoking its code. - Sending native or ERC-20 USDT0 to `address(0)` — both revert on Stable. - Assuming `EXTCODEHASH == 0` means an address is unused. On Stable, permit-based approvals can change state without incrementing nonce. - Writing `value: ethers.parseEther(amount, "ether")` and expecting ETH semantics. That transfer sends USDT0. ``` ### Starter prompts Copy any of these into your AI editor after loading the context block above. #### Deploy a contract ```text Use Foundry to scaffold a project called `stable-escrow`. Write a minimal Escrow contract in Solidity ^0.8.24 with deposit() and withdraw(amount) functions that transfer USDT0 natively. Use address(this).balance for solvency checks (never mirror the balance in a uint256). Reject address(0) recipients. Then produce a deployment command using `forge create` pointed at Stable testnet (RPC https://rpc.testnet.stable.xyz, chain ID 2201). ``` #### Send USDT0 ```text Write a TypeScript script using ethers v6 that sends 0.001 USDT0 natively from the wallet loaded from PRIVATE_KEY. Use base-fee-only EIP-1559 gas (maxPriorityFeePerGas = 0n, maxFeePerGas = 2 * baseFeePerGas). Target Stable testnet. Log the tx hash and a Stablescan explorer URL. ``` #### Set up EIP-7702 delegation ```text Write a TypeScript script using ethers v6 that: 1. Signs an EIP-7702 authorization delegating my EOA to Multicall3 at 0xcA11bde05977b3631167028862bE2a173976CA11 on Stable testnet (chain ID 2201). 2. Sends a type-4 transaction with authorizationList: [signedAuth], to: wallet.address (self-call), and data that invokes aggregate3() to batch three USDT0 transfers (100, 200, 150 USDT0 with 6 decimals). 3. Use maxPriorityFeePerGas: 0n. ``` #### Build a subscription contract ```text Write a SubscriptionManager Solidity contract for EIP-7702 delegation on Stable. It runs on a subscriber's EOA. Expose: - subscribe(bytes32 subId, address provider, uint256 amount, uint256 interval) callable only when msg.sender == address(this) (subscriber on their own EOA). - collect(bytes32 subId) callable only by the registered provider, only when block.timestamp >= nextChargeAt; advances nextChargeAt by interval and transfers USDT0 to the provider. Use IERC20 USDT0 at the testnet address 0x78cf24370174180738c5b8e352b6d14c83a6c9a9. - cancelSubscription(bytes32 subId) callable only by the subscriber. Emit events for SubscriptionCreated, SubscriptionCollected, SubscriptionCancelled. ``` #### Build an x402 pay-per-call API ```text Write an Express server in TypeScript that exposes GET /weather priced at $0.001 USDT0 (amount: "1000", 6 decimals) using @x402/express, @x402/evm/exact/server, and HTTPFacilitatorClient pointed at https://x402.semanticpay.io/. Use Stable mainnet (CAIP-2 eip155:988, USDT0 at 0x779Ded0c9e1022225f8E0630b35a9b54bE713736). The handler should return { weather: "sunny", temperature: 70 }. Read PAY_TO_ADDRESS from env. Print the configured routes on startup. ``` ### Next recommended * [**Paying with MCP server**](/en/how-to/pay-with-mcp) — Wrap a paid API as an MCP tool so AI clients can call and pay for it. * [**Quick start**](/en/tutorial/quick-start) — Pair the AI context with a first-transaction run in five minutes. * [**Difference from Ethereum**](/en/explanation/ethereum-comparison) — Deep-dive on the gas and USDT0 semantics in the context block. ## Index contract events Indexing turns on-chain events into data your application can react to: balance updates, transaction history, UI notifications. This guide shows how to subscribe to events from a deployed Stable contract using ethers.js and how to backfill historical events so you don't miss any emitted while your service was offline. ### Prerequisites * A deployed contract on Stable testnet or mainnet. If you need one, see [Deploy](/en/tutorial/smart-contract) and [Verify](/en/how-to/verify-contract). * Node.js 20 or later. * The contract address and the ABI of the events you want to index. ### 1. Install and configure ```bash npm install ethers ``` ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_TESTNET_WS = "wss://rpc.testnet.stable.xyz"; export const CONTRACT_ADDRESS = "0xDeployedContractAddress"; // Minimal ABI: only the events you want to index. export const CONTRACT_ABI = [ "event NumberUpdated(address indexed caller, uint256 oldValue, uint256 newValue)", ]; ``` ### 2. Subscribe to live events Use a WebSocket provider so you receive events as soon as validators finalize each block. WebSocket avoids polling overhead and keeps notification latency close to block time (\~0.7 seconds on Stable). ```typescript // watchLive.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); contract.on("NumberUpdated", (caller, oldValue, newValue, event) => { console.log("NumberUpdated:"); console.log(" caller: ", caller); console.log(" oldValue: ", oldValue.toString()); console.log(" newValue: ", newValue.toString()); console.log(" tx: ", event.log.transactionHash); console.log(" block: ", event.log.blockNumber); }); console.log("Listening for NumberUpdated events..."); ``` ```bash npx tsx watchLive.ts ``` ```text Listening for NumberUpdated events... NumberUpdated: caller: 0x1234...abcd oldValue: 0 newValue: 42 tx: 0x8f3a...2d41 block: 1284371 ``` Events arrive in real time as callers invoke your contract. ### 3. Backfill historical events When a service starts, you usually need to catch up on events emitted while it was offline. Use `queryFilter` with a block range. ```typescript // backfill.ts import { ethers } from "ethers"; import { STABLE_TESTNET_RPC, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); const latest = await provider.getBlockNumber(); const fromBlock = Math.max(0, latest - 10_000); // last ~10k blocks const events = await contract.queryFilter( contract.filters.NumberUpdated(), fromBlock, latest ); for (const event of events) { console.log( `[block ${event.blockNumber}]`, event.args.caller, "set number to", event.args.newValue.toString() ); } console.log(`Backfilled ${events.length} events from block ${fromBlock} to ${latest}`); ``` ```bash npx tsx backfill.ts ``` ```text [block 1282351] 0x1234...abcd set number to 10 [block 1283092] 0xef01...2345 set number to 25 [block 1284371] 0x1234...abcd set number to 42 Backfilled 3 events from block 1282351 to 1284371 ``` :::warning Wide block ranges (millions of blocks) can exceed RPC rate limits and time out. For production indexers, paginate by 10k-block windows or use [Stablescan's Etherscan-compatible API](/en/how-to/build-p2p-payments#transaction-history) for indexed historical queries. ::: ### 4. Filter events by indexed arguments Events with `indexed` parameters (like `caller` above) can be filtered server-side. Pass the filter value instead of reading every event and filtering in your app. ```typescript // watchUser.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); const userAddress = "0x1234...abcd"; const filter = contract.filters.NumberUpdated(userAddress); contract.on(filter, (caller, oldValue, newValue, event) => { console.log(`${caller} set number to ${newValue.toString()}`); }); console.log(`Watching NumberUpdated for ${userAddress}...`); ``` ```bash npx tsx watchUser.ts ``` ```text Watching NumberUpdated for 0x1234...abcd... 0x1234...abcd set number to 42 ``` ### Handle connection drops WebSocket connections can drop. For production indexers, implement reconnection logic so you don't miss events. ```typescript // resilientWatch.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; let reconnectAttempts = 0; const MAX_RECONNECT = 5; function setupWatcher() { const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); contract.on("NumberUpdated", (caller, oldValue, newValue) => { console.log(`${caller} set number to ${newValue.toString()}`); }); provider.websocket.onerror = (err: any) => { console.error("Provider error:", err); if (reconnectAttempts < MAX_RECONNECT) { reconnectAttempts++; setTimeout(setupWatcher, 5000); } }; } setupWatcher(); ``` ### Next recommended * [**Track unbonding completions**](/en/how-to/track-unbonding) — Index system transaction events (unbonding completions) emitted by the protocol. * [**Build a P2P payment app**](/en/how-to/build-p2p-payments) — Apply indexing to USDT0 Transfer events and build a payment history view. * [**JSON-RPC reference**](/en/reference/json-rpc-api) — See which `eth_getLogs` and related methods Stable supports. ## Index validator data Validator data lives on-chain and is readable over standard EVM JSON-RPC. You query current state through the staking, slashing, and governance precompiles, and you reconstruct history from their event logs. This means an indexer or analytics platform reads everything it needs through `eth_call` and `eth_getLogs`, with no access to a node's `stabled` CLI or Cosmos REST. :::note **Concept:** For what the staking module tracks and how delegation works, see [Staking module](/en/explanation/staking-module). For per-method inputs and outputs, see the [Staking precompile reference](/en/reference/staking-module-api). ::: ### Where each data point comes from | **Data point** | **Source** | **How to read it** | | :-------------------------------- | :-------------------------------------- | :---------------------------------------------------------------- | | Validator name, identity, website | Staking precompile `validators()` | `description.moniker` and related fields | | Stake (bonded tokens) | Staking precompile `validators()` | `tokens` field | | Commission | Staking precompile `validators()` | `commission` field | | Stake changes over time | Staking precompile events | `Delegate`, `Unbond`, `Redelegate` logs | | Join date | Staking precompile event | `CreateValidator` log → block timestamp | | Uptime | Slashing precompile `getSigningInfos()` | `(signedBlocksWindow − missedBlocksCounter) / signedBlocksWindow` | | Voting history (aggregate) | Gov precompile `getTallyResult()` | Per-proposal tally | | Voting history (per validator) | Gov precompile events | `Vote`, `VoteWeighted` logs, voter = operator address | ### Precompile addresses | **Module** | **Address** | **Use for** | | :----------- | :------------------------------------------- | :-------------------------------------------------- | | Staking | `0x0000000000000000000000000000000000000800` | Validator set, stake, commission, delegation events | | Distribution | `0x0000000000000000000000000000000000000801` | Rewards and commission withdrawals | | Gov | `0x0000000000000000000000000000000000000805` | Proposals, tallies, and vote logs | | Slashing | `0x0000000000000000000000000000000000000806` | Signing info and uptime | Connect to Mainnet (Chain ID `988`) at `https://rpc.stable.xyz`. See [Mainnet information](/en/reference/mainnet-information) for endpoints and limits. ### Validator name, stake, and commission Call `validators()` on the staking precompile to read the current validator set. Pass a bond status to filter (for example `BOND_STATUS_BONDED`). Each entry exposes the validator's `description` (including `moniker`), `tokens` (bonded stake), and `commission`. ```typescript // validators.ts import { createPublicClient, http } from "viem"; const STAKING_PRECOMPILE = "0x0000000000000000000000000000000000000800"; const client = createPublicClient({ transport: http("https://rpc.stable.xyz"), }); // See the staking precompile reference for the full validators() ABI and structs. const validators = await client.readContract({ address: STAKING_PRECOMPILE, abi: stakingAbi, functionName: "validators", args: ["BOND_STATUS_BONDED", { key: "0x", offset: 0n, limit: 100n, countTotal: true, reverse: false }], }); for (const v of validators[0]) { console.log(v.description.moniker, v.tokens.toString(), v.commission.toString()); } ``` ```text StableNode-01 4500000000000000000000000 50000000000000000 StableNode-02 3900000000000000000000000 100000000000000000 ``` The `tokens` and `commission` values are scaled to 18 decimals. Divide `commission` by 1e18 to get the rate as a fraction (for example `0.05` for 5%). For the complete `Validator` struct and the `BOND_STATUS_*` values, see the [Staking precompile reference](/en/reference/staking-module-api#validators). ### Stake changes over time `validators()` returns a snapshot. To track how stake moved, index the staking precompile's delegation events. `Delegate`, `Unbond`, and `Redelegate` carry the indexed `validatorAddr` and the `amount`, so you can attribute every stake change to a validator and block. ```typescript // stakeChanges.ts import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: STAKING_PRECOMPILE, event: parseAbiItem( "event Delegate(address indexed delegatorAddr, string indexed validatorAddr, uint256 amount, uint256 newShares)" ), fromBlock: 0n, toBlock: "latest", }); console.log(`${logs.length} delegations indexed`); ``` ```text 1842 delegations indexed ``` `Unbond` and `Redelegate` follow the same shape and additionally carry a `completionTime`. See the [Events section](/en/reference/staking-module-api#events) of the staking reference for exact signatures. ### Join date A validator's join date is the block timestamp of its `CreateValidator` event. The event is indexed by validator address, so you filter for a single validator or sweep the full set, then resolve each log's `blockNumber` to a timestamp with `eth_getBlockByNumber`. ```typescript // joinDate.ts import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: STAKING_PRECOMPILE, event: parseAbiItem("event CreateValidator(address indexed valiAddr, uint256 value)"), fromBlock: 0n, toBlock: "latest", }); for (const log of logs) { const block = await client.getBlock({ blockNumber: log.blockNumber }); console.log(log.args.valiAddr, new Date(Number(block.timestamp) * 1000).toISOString()); } ``` ```text 0xAbc...123 2025-11-04T09:12:44.000Z 0xDef...456 2025-12-18T17:03:01.000Z ``` :::warning Genesis validators have no `CreateValidator` event. They were created in the genesis block, not by a transaction, so no log exists. Treat their join date as the chain genesis: **2025-10-29**. Index `CreateValidator` for everyone who joined after genesis, and backfill the genesis set from the genesis validator list. ::: ### Uptime Read signing information from the slashing precompile (`0x...806`) using `getSigningInfos()`. Each record reports `signedBlocksWindow` (the size of the sliding window) and `missedBlocksCounter` (blocks missed within it). Compute uptime as: ```text uptime = (signedBlocksWindow − missedBlocksCounter) / signedBlocksWindow ``` A validator with a `signedBlocksWindow` of `10000` and a `missedBlocksCounter` of `25` has 99.75% uptime over the window. This is a rolling figure, not lifetime uptime. To track uptime history, snapshot the counters on a fixed interval and store each reading. :::note The slashing precompile follows the Cosmos EVM `x/slashing` interface. Its address is listed in the [system modules precompile table](/en/how-to/use-system-modules#whats-exposed). Generate the exact method ABI from the chain's precompile interface. ::: ### Voting history Governance data has two layers. For the aggregate outcome of a proposal, call `getTallyResult()` on the gov precompile (`0x...805`). For who voted what, index the `Vote` and `VoteWeighted` event logs. The voter address in these logs is the validator's operator address, so you can join votes to validators directly. ```typescript // votes.ts import { parseAbiItem } from "viem"; const GOV_PRECOMPILE = "0x0000000000000000000000000000000000000805"; const logs = await client.getLogs({ address: GOV_PRECOMPILE, event: parseAbiItem( "event Vote(uint64 indexed proposalId, address indexed voter, uint8 option, uint256 weight)" ), fromBlock: 0n, toBlock: "latest", }); console.log(`${logs.length} votes indexed across all proposals`); ``` ```text 38 votes indexed across all proposals ``` Live vote logs are confirmed for every proposal to date (proposals #1 through #7). Use `getTallyResult()` when you only need the final counts per proposal, and the event logs when you need per-validator records. :::note The gov precompile follows the Cosmos EVM `x/gov` interface. Its address is listed in the [system modules precompile table](/en/how-to/use-system-modules#whats-exposed). Generate the exact method ABI and the `VoteOption` enum from the chain's precompile interface. ::: ### Next recommended * [**Staking precompile reference**](/en/reference/staking-module-api) — Look up the full validators(), delegation methods, and event signatures. * [**Create a validator**](/en/how-to/run-validator) — Register a synced node as a validator so it appears in the data above. * [**Indexers and analytics**](/en/reference/indexers) — Browse indexing providers that already serve normalized Stable data. * [**Mainnet information**](/en/reference/mainnet-information) — Get Chain ID, RPC endpoints, and rate limits before you start indexing. This guide provides detailed instructions for installing and setting up a Stable node on various platforms. ### Prerequisites Before starting the installation, ensure you have: * Met all [System Requirements](/en/reference/node-system-requirements) * Root or sudo access to your server * Basic knowledge of Linux command line ### Installation method Use the pre-compiled binaries for your platform. Stable does not currently support building from source. #### Mainnet ##### Linux AMD64 ```bash # Download the latest binary for AMD64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-amd64-mainnet.tar.gz # Extract the archive tar -xvzf stabled-latest-linux-amd64-mainnet.tar.gz # Move binary to system path sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ##### Linux ARM64 ```bash # Download the binary for ARM64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-arm64-mainnet.tar.gz # Extract and install tar -xvzf stabled-latest-linux-arm64-mainnet.tar.gz sudo mv stabled /usr/bin/ # Verify installation stabled version ``` #### Testnet ##### Linux AMD64 ```bash # Download the latest binary for AMD64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-amd64-testnet.tar.gz # Extract the archive tar -xvzf stabled-latest-linux-amd64-testnet.tar.gz # Move binary to system path sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ##### Linux ARM64 ```bash # Download the binary for ARM64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-arm64-testnet.tar.gz # Extract and install tar -xvzf stabled-latest-linux-arm64-testnet.tar.gz sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ### Node initialization After installing the binary, initialize your node: #### Step 1: set node name ```bash # Set your node's moniker (choose a unique name) export MONIKER="your-node-name" ``` #### Step 2: initialize the node #### Mainnet ```bash # Initialize with the mainnet chain ID stabled init $MONIKER --chain-id stable_988-1 # This creates the configuration directory at ~/.stabled/ ``` > **Note**: For current network parameters including chain ID, see [Mainnet Information](/en/reference/mainnet-information) #### Testnet ```bash # Initialize with the testnet chain ID stabled init $MONIKER --chain-id stabletestnet_2201-1 # This creates the configuration directory at ~/.stabled/ ``` > **Note**: For current network parameters including chain ID, see [Testnet Information](/en/reference/testnet-information) #### Step 3: download genesis file :::code-group ```bash [Mainnet] # Create backup of default genesis mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup # Download mainnet genesis wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/genesis.zip unzip genesis.zip # Move genesis to config directory cp genesis.json ~/.stabled/config/genesis.json # Verify genesis checksum sha256sum ~/.stabled/config/genesis.json # Expected: e1ceda79a3cc48a1028ca8646a2e9e2d156f610637cfb8b428ca8354277921f1 ``` ```bash [Testnet] # Create backup of default genesis mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup # Download testnet genesis wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/genesis.zip unzip genesis.zip # Move genesis to config directory cp genesis.json ~/.stabled/config/genesis.json # Verify genesis checksum sha256sum ~/.stabled/config/genesis.json # Expected: 66afbb6e57e6faf019b3021de299125cddab61d433f28894db751252f5b8eaf2 ``` ::: #### Step 4: configure node ##### Download configuration files :::code-group ```bash [Mainnet] # Download optimized configuration (choose one based on your node type) # For RPC/Full nodes: wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/rpc_node_config.zip unzip rpc_node_config.zip # For Archive nodes: # wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/archive_node_config.zip # unzip archive_node_config.zip # Backup original config cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup # Apply new configuration cp config.toml ~/.stabled/config/config.toml # Update moniker in config sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml ``` ```bash [Testnet] # Download optimized configuration wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/rpc_node_config.zip unzip rpc_node_config.zip # Backup original config cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup # Apply new configuration cp config.toml ~/.stabled/config/config.toml # Update moniker in config sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml ``` ::: ##### Essential configuration updates Edit `~/.stabled/config/app.toml`: ```toml # Enable JSON-RPC for EVM compatibility [json-rpc] enable = true address = "0.0.0.0:8545" ws-address = "0.0.0.0:8546" allow-unprotected-txs = true ``` Edit `~/.stabled/config/config.toml`: :::code-group ```toml [Mainnet] # P2P Configuration [p2p] # Maximum number of peers max_num_inbound_peers = 50 max_num_outbound_peers = 30 # Seed nodes seeds = "9aa181b20248e948567cb47a15eae35d58cd549d@seed1.stable.xyz:46656" # Persistent peers (mainnet seed nodes) persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656" # Enable peer exchange pex = true # RPC Configuration [rpc] # Listen address laddr = "tcp://0.0.0.0:26657" # Maximum number of simultaneous connections max_open_connections = 900 # CORS settings (adjust for production) cors_allowed_origins = ["*"] ``` ```toml [Testnet] # P2P Configuration [p2p] # Maximum number of peers max_num_inbound_peers = 50 max_num_outbound_peers = 30 # Seed nodes seeds = "6f3195823f7e5ee6f911a0a0ceb9ea689e0dc5bd@seed1.testnet.stable.xyz:56656" # Persistent peers (testnet seed nodes) persistent_peers = "128accd3e8ee379bfdf54560c21345451c7048c7@peer1.testnet.stable.xyz:26656" # Enable peer exchange pex = true # RPC Configuration [rpc] # Listen address laddr = "tcp://0.0.0.0:26657" # Maximum number of simultaneous connections max_open_connections = 900 # CORS settings (adjust for production) cors_allowed_origins = ["*"] ``` ::: ### Systemd service setup Create a systemd service for automatic management: #### Step 1: create service file :::code-group ```bash [Mainnet] sudo tee /etc/systemd/system/stabled.service > /dev/null < /dev/null <> ~/.bashrc echo "export DAEMON_NAME=stabled" >> ~/.bashrc echo "export DAEMON_HOME=$HOME/.stabled" >> ~/.bashrc echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=true" >> ~/.bashrc echo "export DAEMON_RESTART_AFTER_UPGRADE=true" >> ~/.bashrc echo "export DAEMON_LOG_BUFFER_SIZE=512" >> ~/.bashrc echo "export UNSAFE_SKIP_BACKUP=true" >> ~/.bashrc # Load variables source ~/.bashrc ``` #### Step 3: setup Cosmovisor directory structure ```bash # Create cosmovisor directory structure mkdir -p ~/.stabled/cosmovisor/genesis/bin mkdir -p ~/.stabled/cosmovisor/upgrades # Copy current binary to genesis cp /usr/bin/stabled ~/.stabled/cosmovisor/genesis/bin/ # Create current symlink ln -s ~/.stabled/cosmovisor/genesis ~/.stabled/cosmovisor/current # Verify setup ls -la ~/.stabled/cosmovisor/ cosmovisor run version ``` #### Step 4: set environment variable ```bash # Set service name (default: stable) export SERVICE_NAME=stable ``` #### Step 5: create service file :::code-group ```bash [Mainnet] sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null < /dev/null < /dev/null < /dev/null <` ### Overview The integration flow has three steps: 1. **Build an InnerTx**: the user signs a transaction with `gasPrice = 0`. 2. **Submit to Waiver Server**: submit the signed transaction to the Waiver Server API. 3. **Handle the response**: the waiver server wraps and broadcasts the transaction. Process the streamed results and surface the transaction hash to the user. ### Step 1: create the user's InnerTx The user signs a standard transaction with `gasPrice = 0`. The `to` address and method selector must be permitted by the waiver's `AllowedTarget` policy. ```typescript // config.ts export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet WAIVER_SERVER: "https://waiver.testnet.stable.xyz", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; ``` ```typescript import { ethers } from "ethers"; import { CONFIG } from "./config"; const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ recipientAddress, ethers.parseUnits("0.01", 18) ]); const gasEstimate = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit: gasEstimate, nonce: nonce, chainId: CONFIG.CHAIN_ID, }; const signedInnerTx = await userWallet.signTransaction(innerTx); ``` :::warning `gasPrice` must be `0`. If it is non-zero, the waiver server rejects the transaction. ::: ### Step 2: submit to the Waiver Server ```typescript import { CONFIG } from "./config"; const API_KEY = process.env.WAIVER_API_KEY; const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTx], }), }); ``` #### Batch submissions You can submit multiple signed transactions in a single request: ```typescript body: JSON.stringify({ transactions: [signedTx1, signedTx2, signedTx3], }) ``` Each result line includes an `index` field corresponding to the transaction's position in the array. ### Step 3: handle the response The response is streamed as NDJSON (newline-delimited JSON). Each line corresponds to one submitted transaction. ```typescript const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); if (result.success) { console.log(`tx ${result.index} confirmed: ${result.txHash}`); } else { console.error(`tx ${result.index} failed: ${result.error.message}`); } } } ``` **Success response:** ```json {"index": 0, "id": "abc123", "success": true, "txHash": "0x..."} ``` **Failure response:** ```json {"index": 1, "id": "def456", "success": false, "error": {"code": "VALIDATION_FAILED", "message": "invalid signature"}} ``` ### Error codes | **Code** | **Description** | | :-------------------- | :------------------------------------------------------------------------- | | `PARSE_ERROR` | Failed to parse transaction | | `INVALID_REQUEST` | Malformed request body | | `BATCH_SIZE_EXCEEDED` | Batch size exceeds allowed maximum | | `VALIDATION_FAILED` | Transaction validation failed (e.g., invalid signature, disallowed target) | | `BROADCAST_FAILED` | Failed to broadcast to chain | | `RATE_LIMITED` | Rate limit exceeded | | `QUEUE_FULL` | Server queue at capacity | | `TIMEOUT` | Request timed out | ### API reference #### GET `/v1/health` Health check endpoint. Authentication: none. #### POST `/v1/submit` Submit a batch of signed inner transactions. Authentication: required (Bearer). **Request body:** ```json { "transactions": ["0x", "0x"] } ``` Response is streamed as NDJSON. Each line corresponds to a submitted transaction index. #### GET `/v1/submit` WebSocket interface for streaming submissions. Authentication: required (Bearer). ### Key takeaways * Gas Waiver is a server-side integration: your backend submits signed user transactions to the Waiver Server. Users never interact with the Waiver Server directly. * The user always signs the InnerTx, preserving signature integrity. The waiver cannot modify the user's transaction. * The target contract must be on the waiver's `AllowedTarget` list. ### Next recommended * [**Zero gas transactions**](/en/how-to/zero-gas-transactions) — See the demo-focused flow and how to verify zero gas on a receipt. * [**Self-hosted Gas Waiver**](/en/how-to/self-hosted-gas-waiver) — Run your own waiver without the hosted API. * [**Gas waiver protocol**](/en/reference/gas-waiver-api) — Full wrapper transaction spec and governance model. * [**Stable SDK**](/en/explanation/sdk-overview) — Use the typed client to sign user transactions you then submit to the Waiver Server. Comprehensive guide for monitoring Stable nodes and performing routine maintenance tasks. ### Monitoring stack overview #### Recommended stack * **Prometheus**: Metrics collection * **Grafana**: Visualization and dashboards * **AlertManager**: Alert routing and management * **Node Exporter**: System metrics * **Loki**: Log aggregation (optional) ### Quick monitoring setup #### Step 1: enable Prometheus metrics ```toml # Edit ~/.stabled/config/config.toml [instrumentation] prometheus = true prometheus_listen_addr = ":26660" namespace = "stablebft" ``` Restart node: ```bash sudo systemctl restart ${SERVICE_NAME} ``` #### Step 2: install Prometheus ```bash # Download Prometheus wget https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz tar xvf prometheus-2.45.0.linux-amd64.tar.gz sudo mv prometheus-2.45.0.linux-amd64 /opt/prometheus # Create config sudo tee /opt/prometheus/prometheus.yml > /dev/null < /dev/null < 3 | | `stablebft_consensus_block_interval` | Block time | > 10s | | `stablebft_p2p_peers` | Connected peers | \< 3 | | `stablebft_mempool_size` | Mempool size | > 1500 | | `stablebft_mempool_failed_txs` | Failed transactions | > 100/min | #### System metrics | Metric | Description | Alert Threshold | | ---------------------------------- | ---------------- | ---------------- | | `node_cpu_seconds_total` | CPU usage | > 80% for 5m | | `node_memory_MemAvailable_bytes` | Available memory | \< 10% | | `node_filesystem_avail_bytes` | Available disk | \< 10% | | `node_network_receive_bytes_total` | Network RX | > 100MB/s | | `node_disk_io_time_seconds_total` | Disk I/O | > 80% | | `node_load15` | System load | > CPU cores \* 2 | ### Grafana dashboard setup #### Import Stable dashboard ```json { "dashboard": { "title": "Stable Node Monitoring", "panels": [ { "title": "Block Height", "targets": [ { "expr": "stablebft_consensus_height{chain_id=\"stabletestnet_2201-1\"}" } ] }, { "title": "Peers", "targets": [ { "expr": "stablebft_p2p_peers" } ] }, { "title": "Block Time", "targets": [ { "expr": "rate(stablebft_consensus_height[1m]) * 60" } ] }, { "title": "Mempool Size", "targets": [ { "expr": "stablebft_mempool_size" } ] } ] } } ``` #### Custom dashboard import Import dashboards via Grafana UI: ```bash # Navigate to Dashboards > Import > Upload JSON file # Or use Dashboard ID in Grafana's dashboard library ``` ### AlertManager configuration #### Install AlertManager ```bash # Download AlertManager wget https://github.com/prometheus/alertmanager/releases/download/v0.26.0/alertmanager-0.26.0.linux-amd64.tar.gz tar xvf alertmanager-0.26.0.linux-amd64.tar.gz sudo mv alertmanager-0.26.0.linux-amd64 /opt/alertmanager # Configure sudo tee /opt/alertmanager/alertmanager.yml > /dev/null < 1500 for: 10m labels: severity: warning annotations: summary: "High mempool size: {{ $value }}" - alert: DiskSpaceLow expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1 for: 5m labels: severity: critical annotations: summary: "Low disk space: {{ $value | humanizePercentage }}" - alert: HighCPUUsage expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 10m labels: severity: warning annotations: summary: "High CPU usage: {{ $value }}%" ``` ### Log monitoring #### Systemd logs ```bash # View recent logs sudo journalctl -u ${SERVICE_NAME} -n 100 # Follow logs sudo journalctl -u ${SERVICE_NAME} -f # Filter by time sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" # Export logs sudo journalctl -u ${SERVICE_NAME} --since today > stable-logs-$(date +%Y%m%d).log ``` #### Log analysis scripts ```bash #!/bin/bash # analyze-logs.sh # Count errors in last hour echo "Errors in last hour:" sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -c ERROR # Show peer connections echo "Peer connections:" sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep "Peer connection" | tail -10 # Check for consensus issues echo "Consensus rounds:" sudo journalctl -u ${SERVICE_NAME} --since "30 minutes ago" | grep -E "enterNewRound|Timeout" | tail -20 # Memory usage patterns echo "Memory warnings:" sudo journalctl -u ${SERVICE_NAME} --since "1 day ago" | grep -i memory ``` #### Loki setup (optional) ```bash # Install Loki wget https://github.com/grafana/loki/releases/download/v2.9.0/loki-linux-amd64.zip unzip loki-linux-amd64.zip sudo mv loki-linux-amd64 /usr/local/bin/loki # Install Promtail wget https://github.com/grafana/loki/releases/download/v2.9.0/promtail-linux-amd64.zip unzip promtail-linux-amd64.zip sudo mv promtail-linux-amd64 /usr/local/bin/promtail # Configure Promtail sudo tee /etc/promtail-config.yml > /dev/null < ~/reports/daily_$(date +%Y%m%d).log curl -s localhost:26657/status | jq >> ~/reports/daily_$(date +%Y%m%d).log ``` #### Weekly maintenance ```bash #!/bin/bash # weekly-maintenance.sh # Prune old data stabled prune # Compact database stabled compact # Update peer list wget https://raw.githubusercontent.com/stable-chain/networks/main/testnet/peers.txt cat peers.txt >> ~/.stabled/config/config.toml # Create snapshot (optional) ./create-snapshot.sh # System updates sudo apt update sudo apt upgrade -y # Restart node (during low activity) sudo systemctl restart ${SERVICE_NAME} ``` #### Database maintenance ```bash # Check database size du -sh ~/.stabled/data/ # Analyze database stabled debug db stats ~/.stabled/data ``` ### Performance monitoring #### Resource usage tracking ```bash #!/bin/bash # track-resources.sh while true; do TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') CPU=$(top -bn1 | grep "stabled" | awk '{print $9}') MEM=$(top -bn1 | grep "stabled" | awk '{print $10}') IO=$(iostat -x 1 2 | tail -n2 | awk '{print $14}') echo "$TIMESTAMP,CPU:$CPU,MEM:$MEM,IO:$IO" >> ~/metrics/resources.csv sleep 60 done ``` #### Query performance ```bash # Monitor RPC response times while true; do START=$(date +%s%N) curl -s http://localhost:26657/status > /dev/null END=$(date +%s%N) DIFF=$((($END - $START) / 1000000)) echo "RPC response time: ${DIFF}ms" sleep 5 done ``` ### Monitoring best practices 1. **Set up redundant monitoring** * Use external monitoring services * Implement cross-node monitoring * Set up dead man's switch alerts 2. **Alert fatigue prevention** * Tune alert thresholds based on baseline * Use alert grouping and inhibition * Implement escalation policies 3. **Data retention** * Keep metrics for 30 days minimum * Archive important logs * Regular backup of monitoring configs 4. **Security** * Secure Grafana with strong passwords * Use HTTPS for all endpoints * Restrict prometheus access 5. **Documentation** * Document all custom metrics * Maintain runbooks for alerts * Keep dashboard descriptions updated ### Next steps * [Review Troubleshooting Guide](/en/how-to/troubleshoot-node) for issue resolution * [Configure Upgrades](/en/how-to/upgrade-node) with monitoring * Set up custom alerts based on your requirements ## Paying with invoice This guide walks through settling an invoice on-chain using [ERC-3009](/en/explanation/erc-3009) with a deterministic nonce derived from invoice metadata. The nonce links each payment to its invoice and prevents double payment. :::note **Concept:** For the invoice settlement model and comparison to traditional B2B invoicing, see [Invoice settlement](/en/reference/invoices). ::: ### What you'll build A full invoice lifecycle: the buyer signs an ERC-3009 authorization off-chain, the vendor submits it on-chain, and reconciliation matches the resulting `AuthorizationUsed` event back to the invoice by deterministic nonce. #### Demo ```text step 1. Invoice issued number: INV-2026-001234 amount: 5000 USDT0 dueDate: 2026-04-30 step 2. Buyer signs authorization (off-chain, no gas) nonce: 0xa1b2...c3d4 (from invoice metadata) signature: 0xf0e9...1234 step 3. Vendor submits transferWithAuthorization tx: 0x8f3a...2d41 amount: 5000 USDT0 transferred to vendor step 4. Reconciliation AuthorizationUsed(nonce=0xa1b2...) → invoice INV-2026-001234 Transfer event verified for correct amount and parties ERP: marked PAID at block 1284371 ``` ### Overview **Buyer:** ``` ─── Buyer ─────────────────────────────────────────── nonce = getInvoiceNonce(invoice) authorization = { from: buyer, to: vendor, value: amount, nonce, ... } signature = signTypedData(authorization) // Option A: Buyer submits the transaction directly. usdt0.transferWithAuthorization(authorization, signature) // Option B: Buyer sends {authorization, signature} to the vendor. // The vendor (or a facilitator) submits on the buyer's behalf. ``` **Vendor:** ``` ─── Vendor ────────────────────────────────────────── // If Option B: submit transferWithAuthorization using the buyer's signature // Reconcile via AuthorizationUsed event on AuthorizationUsed(authorizer, nonce): invoice = nonceToInvoice.get(nonce) transferLog = receipt.logs.find(Transfer matching invoice.buyer, invoice.vendor, invoice.amount) if transferLog: erpSystem.markPaid(invoice.id, txHash, settledAt) ``` ### Configuration ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const EIP712_DOMAIN = { name: "USDT0", version: "1", chainId: CHAIN_ID, verifyingContract: USDT0_ADDRESS, }; export const TRANSFER_WITH_AUTHORIZATION_TYPE = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; export interface Invoice { number: string; // e.g. "INV-2026-001234" vendor: string; // vendor wallet address buyer: string; // buyer wallet address amount: bigint; // amount in USDT0 atomic units (6 decimals) dueDate: number; // Unix timestamp } ``` ### Step 1: Generate a deterministic nonce Both the buyer and the vendor can independently compute the same nonce from invoice metadata. No external registry is needed. ```typescript // nonce.ts import { ethers } from "ethers"; import { Invoice } from "./config"; export function getInvoiceNonce(invoice: Invoice): string { return ethers.solidityPackedKeccak256( ["string", "address", "address", "uint256", "uint256"], [ invoice.number, invoice.vendor, invoice.buyer, invoice.amount, invoice.dueDate, ] ); } // Example const invoice: Invoice = { number: "INV-2026-001234", vendor: "0xVendorAddress", buyer: "0xBuyerAddress", amount: ethers.parseUnits("5000", 6), // 5,000 USDT0 dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000), }; const nonce = getInvoiceNonce(invoice); // Same input always produces the same nonce. // This nonce is consumed on-chain upon payment, preventing double payment. ``` ### Step 2: Sign the authorization (buyer) The buyer signs an ERC-3009 `transferWithAuthorization` using the deterministic nonce from Step 1. ```typescript // sign-invoice.ts import { ethers } from "ethers"; import { provider, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPE, Invoice, } from "./config"; import { getInvoiceNonce } from "./nonce"; const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider); async function signInvoiceAuthorization(invoice: Invoice) { const nonce = getInvoiceNonce(invoice); const gracePeriod = 30 * 24 * 60 * 60; // 30 days after due date const authorization = { from: invoice.buyer, to: invoice.vendor, value: invoice.amount, validAfter: 0, validBefore: invoice.dueDate + gracePeriod, nonce, }; const signature = await buyerWallet.signTypedData( EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPE, authorization ); return { authorization, signature }; } ``` ### Step 3: Submit the transaction Two options depending on who submits. #### Option A: Buyer submits The buyer submits the `transferWithAuthorization` transaction directly and pays gas. Use this when the buyer controls when and how the payment is executed, for example when the buyer's accounting system needs the tx hash tied to an internal approval flow. ```typescript // pay.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider); const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", ], buyerWallet, ); async function payInvoice( authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string }, signature: string, ) { const { v, r, s } = ethers.Signature.from(signature); const tx = await usdt0.transferWithAuthorization( authorization.from, authorization.to, authorization.value, authorization.validAfter, authorization.validBefore, authorization.nonce, v, r, s, ); const receipt = await tx.wait(1); console.log("Invoice paid, tx:", receipt.hash); // The nonce is now consumed; the same invoice cannot be paid twice. return { txHash: receipt.hash, blockNumber: receipt.blockNumber }; } ``` #### Option B: Vendor submits The buyer sends `{authorization, signature}` to the vendor through API, email, or any channel. The vendor (or a facilitator) submits the transaction on the buyer's behalf, so the buyer does not need to manage gas. Use this when the vendor needs synchronous confirmation within the same request flow. ```typescript // settle.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const vendorWallet = new ethers.Wallet(process.env.VENDOR_KEY!, provider); const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", ], vendorWallet, ); async function settleInvoice( authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string }, signature: string, ) { const { v, r, s } = ethers.Signature.from(signature); const tx = await usdt0.transferWithAuthorization( authorization.from, authorization.to, authorization.value, authorization.validAfter, authorization.validBefore, authorization.nonce, v, r, s, ); const receipt = await tx.wait(1); console.log("Invoice settled, tx:", receipt.hash); return { txHash: receipt.hash, blockNumber: receipt.blockNumber }; } ``` ### Step 4: Reconcile via on-chain events (vendor) Regardless of who submitted the transaction, every invoice payment emits an `AuthorizationUsed` event carrying the deterministic nonce. The vendor listens for this event and matches it to a pending invoice by nonce. Because the nonce is derived from invoice metadata, matching is exact. :::note Matching by nonce identifies which invoice was paid, but the vendor should also verify the `Transfer` event in the same transaction to confirm that the correct amount was sent to the correct recipient. The code below includes this verification. ::: ```typescript // reconcile.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS, Invoice } from "./config"; import { getInvoiceNonce } from "./nonce"; const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)", "event Transfer(address indexed from, address indexed to, uint256 value)", ], provider, ); // Build a lookup map: nonce -> invoice. // In production, this comes from your invoice database. const invoices: Invoice[] = [ { number: "INV-2026-001234", vendor: "0xVendorAddress", buyer: "0xBuyerAddress", amount: ethers.parseUnits("5000", 6), dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000), }, ]; const nonceToInvoice = new Map(); for (const inv of invoices) { nonceToInvoice.set(getInvoiceNonce(inv), inv); } usdt0.on("AuthorizationUsed", async (authorizer: string, nonce: string, event: any) => { const invoice = nonceToInvoice.get(nonce); if (!invoice) return; // not one of our invoices const receipt = await event.getTransactionReceipt(); const transferLog = receipt.logs .map((log: any) => { try { return usdt0.interface.parseLog(log); } catch { return null; } }) .find( (parsed: any) => parsed?.name === "Transfer" && parsed.args[0].toLowerCase() === invoice.buyer.toLowerCase() && parsed.args[1].toLowerCase() === invoice.vendor.toLowerCase() && parsed.args[2] === invoice.amount ); if (!transferLog) { console.error("No matching Transfer event for invoice:", invoice.number); return; } // All checks passed console.log(`Invoice ${invoice.number} PAID`); console.log(" tx:", receipt.hash); console.log(" settled at block:", receipt.blockNumber); // In production: update your ERP/accounting system here // erpSystem.markPaid(invoice.number, receipt.hash, receipt.blockNumber); }); console.log("Listening for invoice settlements..."); ``` ```bash npx tsx reconcile.ts ``` ```text Listening for invoice settlements... Invoice INV-2026-001234 PAID tx: 0x8f3a...2d41 settled at block: 1284371 ``` ### Handle failed payments A submitted `transferWithAuthorization` can revert for several reasons. Detect and surface each one to the vendor or buyer so the invoice can be retried or closed. | **Revert reason** | **Cause** | **Recovery** | | :----------------------------------------------- | :------------------------------------------------------------------------ | :------------------------------------------------------------------ | | `FiatTokenV2: invalid signature` | Signature doesn't match the authorization fields. | Ask buyer to re-sign with unchanged invoice data. | | `FiatTokenV2: authorization is used or canceled` | Nonce was already consumed (double-submission) or the buyer cancelled it. | Mark the invoice as already-paid; look up the original tx by nonce. | | `FiatTokenV2: authorization is not yet valid` | Submitted before `validAfter`. | Wait until `validAfter` or issue a new authorization. | | `FiatTokenV2: authorization is expired` | Submitted after `validBefore`. | Issue a new authorization with an extended window. | | `FiatTokenV2: transfer amount exceeds balance` | Buyer's USDT0 balance is insufficient. | Notify buyer to fund their wallet, then retry the same signature. | Catch reverts and classify them before retrying. ```typescript // retry.ts import { ethers } from "ethers"; async function submitWithRetry( submit: () => Promise, ): Promise { try { const tx = await submit(); const receipt = await tx.wait(1); return receipt!.hash; } catch (err: any) { const reason = err?.info?.error?.message || err?.reason || err?.message || ""; if (reason.includes("authorization is used or canceled")) { // Lookup the original tx by AuthorizationUsed event; mark invoice paid. throw new Error("ALREADY_PAID"); } if (reason.includes("authorization is expired")) { throw new Error("AUTHORIZATION_EXPIRED"); } if (reason.includes("invalid signature")) { throw new Error("INVALID_SIGNATURE"); } if (reason.includes("transfer amount exceeds balance")) { throw new Error("INSUFFICIENT_BALANCE"); } throw err; } } ``` :::warning Never retry a failed submission without classifying the error. Blind retries on a reverted transferWithAuthorization can pass validation after the buyer tops up their balance, which may not match the buyer's latest intent. ::: ### Next recommended * [**Invoice settlement concept**](/en/reference/invoices) — Understand the deterministic-nonce reconciliation model. * [**ERC-3009**](/en/explanation/erc-3009) — Review the signed-authorization standard behind this flow. * [**Enable gas-free transactions**](/en/how-to/integrate-gas-waiver) — Combine with Gas Waiver to eliminate gas from the settlement path. ## Paying with MCP server This guide shows how to bridge x402-enabled APIs to [MCP](https://modelcontextprotocol.io) tools so AI clients can call and pay for them through natural-language prompts. It builds on the server from [Build a pay-per-call API](/en/how-to/build-pay-per-call). ### What you'll build An MCP server that wraps x402-paid endpoints as tools. The AI client types a natural-language prompt, each tool call triggers a paid x402 request, and settlement is visible on Stablescan. The user never sees a wallet prompt. #### Demo ```text step 1. User in Claude: "Pull financials for ACME Corp and assess credit risk." step 2. Client calls get_company_financials("ACME") → MCP handler: fetchWithPayment("/financials?ticker=ACME") → 402 Payment Required → sign ERC-3009 → retry → Facilitator settles $0.01 USDT0 on-chain → tx: 0x8f3a...aaaa → 200 OK { revenue, debt_ratio, cash_flow } step 3. Client calls assess_credit_risk(financials) → MCP handler: fetchWithPayment("/credit-risk", POST) → Facilitator settles $0.05 USDT0 on-chain → tx: 0x9bc4...bbbb → 200 OK { score: 72, rating: "moderate" } step 4. Claude responds: "ACME Corp has a credit risk score of 72 (moderate). Revenue is stable but debt-to-equity ratio is elevated at 1.8x..." ``` Both `tx` values are visible on [https://stablescan.xyz](https://stablescan.xyz). :::note **Agent wallet funding**: The MCP server signs payments with a seed phrase you control. Fund that wallet with USDT0 on mainnet before starting the server. A balance of at least `$0.10` covers several paid calls; `$1.00` is plenty for extended testing. Top up as needed using a standard USDT0 transfer to the wallet's address. ::: ### Overview **MCP Server:** ```typescript // --- MCP Server --- // Bridge x402-enabled APIs to MCP tools tools = { "get_company_financials": { handler: (ticker) => fetchWithPayment("https://api.example.com/financials?ticker=" + ticker), }, "assess_credit_risk": { handler: (financials) => fetchWithPayment("https://api.example.com/credit-risk", { method: "POST", body: JSON.stringify({ financials }), }), }, } ``` **User (via AI client):** ``` ─── AI Client ─────────────────────────────────────── User: "Pull financials for ACME Corp and assess their credit risk." Client calls get_company_financials tool → MCP server sends x402 paid request → Facilitator settles USDT0 on-chain → API returns financial data Client calls assess_credit_risk tool with the result → MCP server sends x402 paid request → Facilitator settles USDT0 on-chain → API returns risk assessment → Client responds with the combined result ``` ### Prerequisites * A running x402 server (see [Build a pay-per-call API](/en/how-to/build-pay-per-call)). * An MCP-compatible AI client (Claude Desktop, Claude Code, etc.). ### Step 1: Create the MCP server The MCP server acts as a bridge between AI clients and x402-enabled APIs. Each tool makes a paid request using the x402 client SDK and returns the result. ```bash npm install @modelcontextprotocol/sdk @x402/fetch @x402/evm @tetherto/wdk-wallet-evm ``` ```typescript // mcp-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; import { z } from "zod"; // --- Wallet and x402 client --- const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, { provider: "https://rpc.stable.xyz", }).getAccount(0); const client = new x402Client(); registerExactEvmScheme(client, { signer: account }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); // --- x402 API base URL --- const API_BASE = process.env.API_BASE || "http://localhost:4021"; // --- MCP server --- const server = new McpServer({ name: "x402-payments", version: "1.0.0", }); server.tool( "get_company_financials", "Get company financial data by ticker (paid endpoint, $0.01 per call)", { ticker: z.string().describe("Company ticker symbol (e.g. ACME)") }, async ({ ticker }) => { const response = await fetchWithPayment(`${API_BASE}/financials?ticker=${ticker}`); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; }, ); server.tool( "assess_credit_risk", "Assess credit risk from financial data (paid endpoint, $0.05 per call)", { financials: z.string().describe("JSON string of company financial data") }, async ({ financials }) => { const response = await fetchWithPayment(`${API_BASE}/credit-risk`, { method: "POST", headers: { "Content-Type": "application/json" }, body: financials, }); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; }, ); server.tool( "check_balance", "Check the USDT0 balance of the payment wallet", {}, async () => { const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const balance = await account.getTokenBalance(USDT0_STABLE); const formatted = (Number(balance) / 1e6).toFixed(2); return { content: [{ type: "text", text: `Wallet balance: ${formatted} USDT0` }], }; }, ); // --- Start --- const transport = new StdioServerTransport(); await server.connect(transport); ``` Each tool handler calls `fetchWithPayment`, which handles the full x402 payment cycle automatically. The AI client only sees the tool name, description, and parameters. ### Step 2: Configure your AI client Add the MCP server to your AI client's configuration. **Claude Desktop** (`claude_desktop_config.json`): ```json { "mcpServers": { "x402-payments": { "command": "npx", "args": ["tsx", "/path/to/mcp-server.ts"], "env": { "SEED_PHRASE": "your seed phrase here", "API_BASE": "https://api.example.com" } } } } ``` **Claude Code:** ```bash claude mcp add x402-payments -- npx tsx /path/to/mcp-server.ts ``` After configuration, restart your AI client. The tools should appear in the available tool list. :::warning The seed phrase in the MCP configuration controls real funds. Store it securely using your OS keychain or a secrets manager rather than in plain-text config files. ::: ### Step 3: Type the prompt and use it Once configured, the AI client can call paid APIs through the user's prompt: **User:** "Pull financials for ACME Corp and assess their credit risk." 1. Client calls `get_company_financials("ACME")`: $0.01 paid via x402. Returns revenue, debt ratio, cash flow, etc. 2. Client calls `assess_credit_risk(financials)`: $0.05 paid via x402. Returns risk score, rating, key factors. 3. Client responds: "ACME Corp has a credit risk score of 72 (moderate). Revenue is stable but debt-to-equity ratio is elevated at 1.8x..." Individual tools also work on their own: * "Pull financials for ACME Corp" calls `get_company_financials` ($0.01). * "Assess credit risk for this data" calls `assess_credit_risk` ($0.05). * "How much USDT0 do I have left?" calls `check_balance`. The user does not interact with wallets, signatures, or payment flows. The MCP server handles payment for each tool call transparently. ### Spending controls To prevent unexpected spending, consider adding controls to the MCP server. ```typescript const MAX_PER_CALL = 100_000; // $0.10 in base units const MAX_PER_SESSION = 5_000_000; // $5.00 in base units let sessionSpent = 0n; function checkSpendingLimit(amount: bigint) { if (amount > BigInt(MAX_PER_CALL)) { throw new Error(`Amount exceeds per-call limit of $${MAX_PER_CALL / 1e6}`); } if (sessionSpent + amount > BigInt(MAX_PER_SESSION)) { throw new Error(`Session spending limit of $${MAX_PER_SESSION / 1e6} reached`); } sessionSpent += amount; } ``` These limits run server-side. The AI client cannot modify or bypass them. ### Next recommended * [**Build a pay-per-call API**](/en/how-to/build-pay-per-call) — Set up the x402 server this MCP server bridges. * [**x402 concept**](/en/explanation/x402) — Review the settlement protocol behind these payments. * [**Develop with AI**](/en/how-to/develop-with-ai) — Wire Stable's Docs and Runtime MCP servers into the same AI client. ## Production readiness Work through each section below before switching from testnet to mainnet. ### Before you launch * **Network targets.** Your application reads mainnet values, not testnet: chain ID `988`, RPC `https://rpc.stable.xyz`, explorer `https://stablescan.xyz`. Full configuration is in [Connect](/en/reference/connect). * **Contracts verified.** Deployed contracts are verified on [stablescan.xyz](https://stablescan.xyz) so users and partners can inspect them. * **Mainnet funding path.** You have a documented way for production wallets to acquire USDT0: direct, bridge via LayerZero, or custodian. Faucets are testnet-only. * **Environment isolation.** Keys, RPC credentials, and signing paths are separated between testnet and mainnet. ### Security checks USDT0's dual-role behavior breaks a handful of assumptions ported from Ethereum. Each item below should be validated. The full list is in the [migration checklist](/en/explanation/usdt0-behavior). **Solvency checks read real native balance, not a mirror.** :::warning Tracking deposited native value in an internal variable is unsafe. An external `USDT0.transferFrom` call can drain the contract's native balance without invoking any contract code. ::: ```solidity // SAFE — checks real balance at the moment of transfer function withdraw() external { uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount, "insufficient balance"); payable(msg.sender).call{value: amount}(""); } ``` **Allowance-based drain paths are covered by tests.** Every `approve` / `transferFrom` / `permit` path has a test that attempts to drain the contract's native balance. **Zero-address transfers are rejected before the call.** :::warning Both native and ERC-20 transfers to `address(0)` revert on Stable. Validate recipients explicitly, or your transaction will fail. ::: ```solidity require(recipient != address(0), "zero address recipient"); payable(recipient).call{value: amount}(""); ``` **Address-reuse detection does not rely on `EXTCODEHASH`.** Permit-based approvals change native balance without a nonce increment, so `EXTCODEHASH` can oscillate between zero hash and empty hash. Use explicit tracking instead. ### Performance and reliability * **RPC redundancy.** Production traffic has a failover plan. Third-party providers are listed in [RPC providers](/en/reference/rpc-providers). * **Gas estimation.** Transactions set `maxPriorityFeePerGas` to `0` and compute `maxFeePerGas` from the current base fee. See [Gas pricing](/en/reference/gas-pricing-api). * **Block time.** Blocks are produced roughly every 0.7 seconds with single-slot finality. Poll intervals and confirmation thresholds are tuned to this cadence. * **Retries.** Transient RPC errors are retried idempotently. For financially sensitive flows, inclusion is verified via receipts or logs before downstream state changes. ### Operational ownership * **Monitoring.** If you run your own nodes, alerts watch block production, peer health, and RPC latency; see [Monitoring](/en/how-to/monitor-node). If you use a third-party RPC, track provider SLAs and failover telemetry. * **Upgrades.** Protocol releases are tracked so node operators can schedule upgrades; see [Mainnet version history](/en/reference/mainnet-version-history). * **Runbooks.** Rollback procedures exist for contract pauses, key rotation, and RPC provider switches. ### Support and escalation * [Developer assistance](/en/reference/developer-assistance): FAQ and reference pointers. * [Discord](https://discord.gg/stablexyz): community support and protocol updates. * `bizdev@stable.xyz`: partnership and integration conversations. ### Next recommended * [**USDT0 behavior**](/en/explanation/usdt0-behavior) — Read the full migration checklist and contract design requirements. * [**Mainnet information**](/en/reference/mainnet-information) — Check mainnet chain parameters and version history. * [**RPC providers**](/en/reference/rpc-providers) — Pick third-party RPC providers for redundancy. * [**Monitoring**](/en/how-to/monitor-node) — Wire metrics and alerts for block production and RPC health. A validator is a synced full node that has registered on-chain and bonded stake. You install and sync the node first, then register it by calling `createValidator` on the staking precompile (`0x0000000000000000000000000000000000000800`). This page covers the registration step. For the node itself, see [Install a node](/en/how-to/install-node) and [Node configuration](/en/reference/node-configuration). :::warning Register only after your node is fully synced. A validator that signs before it has caught up can double-sign and be permanently removed from the set (tombstoned). Set `double_sign_check_height = 2` or higher in `config.toml` before you start (see [Node configuration](/en/reference/node-configuration#consensus-configuration)). Setting it to `1` performs no check. ::: ### Prerequisites * A fully synced full node on Mainnet (Chain ID `988`). See [Install a node](/en/how-to/install-node). * `double_sign_check_height` set to `2` or higher in `~/.stabled/config/config.toml`. * [Foundry](https://book.getfoundry.sh/) installed for `cast`, used to call the precompile. * The staking amount funded on your validator's EVM address, in USDT0. Confirm the node has caught up before going further. `catching_up` must be `false`. ```bash curl -s localhost:26657/status | jq '.result.sync_info.catching_up' ``` ```text false ``` ### Step 1: prepare validator keys Create the operator account, then read the two values `createValidator` needs: the consensus public key (base64) and the validator's EVM address. ```bash # Create the validator operator account stabled keys add validator # Consensus public key (base64) — save this stabled comet show-validator | jq .key # Derive the validator's EVM address (0x form) stabled keys parse $(stabled keys show validator -a) ``` ```text "AbCd...base64PubKey...==" # ... # then, evm address is 0xCAEA59C7476C87D0FF6BE6F04DA207601D5BE7D0 ``` :::warning Back up `~/.stabled/config/priv_validator_key.json` offline and never run two nodes with the same key. Two instances signing with one key is double-signing and results in a permanent slash. ::: ### Step 2: set up environment ```bash # Staking precompile contract address export STAKING_ADDRESS="0x0000000000000000000000000000000000000800" # Mainnet EVM RPC export RPC_URL="https://rpc.stable.xyz" # Your operator private key and validator EVM address export PRIVATE_KEY="your_private_key_here" export VALIDATOR_ADDRESS="0xYourValidatorAddress" # Consensus pubkey from Step 1 export PUBKEY="AbCd...base64PubKey...==" # Self-delegation amount in wei (18 decimals). 1000000000000000000 = 1 token export AMOUNT="1000000000000000000" ``` ### Step 3: create the validator Call `createValidator` on the staking precompile. The function takes a `description` tuple, a `commissionRates` tuple, the minimum self-delegation, the validator address, the consensus pubkey, and the bonded amount. Encode and send it with `cast`. ```bash # createValidator( # (moniker, identity, website, securityContact, details), # (rate, maxRate, maxChangeRate), # minSelfDelegation, validatorAddress, pubkey, value # ) cast send "$STAKING_ADDRESS" \ "createValidator((string,string,string,string,string),(uint256,uint256,uint256),uint256,address,string,uint256)" \ "(\"My Validator\",\"keybase-id\",\"https://example.com\",\"security@example.com\",\"My validator description\")" \ "(100000000000000000,200000000000000000,10000000000000000)" \ "1000000000000000000" \ "$VALIDATOR_ADDRESS" \ "$PUBKEY" \ "$AMOUNT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ```text transactionHash 0x4f...c2 status 1 (success) ``` The commission tuple is `(rate, maxRate, maxChangeRate)`, each scaled to 18 decimals. The example sets a 10% rate (`100000000000000000`), a 20% ceiling, and a 1% maximum daily change. `maxRate` and `maxChangeRate` are fixed at creation and cannot be edited later. A successful call emits a `CreateValidator` event. See the [staking precompile reference](/en/reference/staking-module-api#createvalidator) for every field. ### Step 4: verify Confirm the validator is registered and bonded by reading it back from the staking precompile, then check it is signing blocks. ```bash # Read your validator's on-chain record cast call "$STAKING_ADDRESS" \ "validator(address)" "$VALIDATOR_ADDRESS" \ --rpc-url "$RPC_URL" # Confirm the node reports validator info curl -s localhost:26657/status | jq '.result.validator_info' ``` ```text # validator() returns the moniker, tokens, commission, and a bonded status (3) # validator_info shows your consensus address with non-zero voting power ``` ### Add self-delegation To bond more stake to your own validator after creation, call `delegate` on the same precompile. ```bash cast send "$STAKING_ADDRESS" \ "delegate(address,address,uint256)" \ "$VALIDATOR_ADDRESS" "$VALIDATOR_ADDRESS" "$AMOUNT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ```text status 1 (success) ``` ### After registration Keep the validator healthy and ready for network upgrades: * **Monitor signing and missed blocks** with the Prometheus and Grafana stack in [Monitor a node](/en/how-to/monitor-node). * **Automate upgrades** so you don't miss an upgrade height. See the Cosmovisor setup in [Install a node](/en/how-to/install-node#cosmovisor-setup-recommended-for-automatic-upgrades) and [Upgrade a node](/en/how-to/upgrade-node). * **Diagnose problems** (not syncing, not signing) with [Troubleshoot a node](/en/how-to/troubleshoot-node). ### Next recommended * [**Staking precompile reference**](/en/reference/staking-module-api) — Look up the full createValidator, delegate, and editValidator signatures and structs. * [**Node configuration**](/en/reference/node-configuration) — Set double\_sign\_check\_height and other validator-critical config before you register. * [**Monitor a node**](/en/how-to/monitor-node) — Track signing, missed blocks, and resource usage so you catch problems before a slash. * [**Index validator data**](/en/how-to/index-validator-data) — Read your validator's stake, uptime, and voting history on-chain once it is live. ## Use the SDK with viem `@stablechain/sdk` is built on viem. `createStable` accepts three signing modes, and you pick one based on where the code runs: server-side with a private key, browser-side with the user's wallet, or with a `WalletClient` you've already constructed (for example, in a wagmi app). This guide shows each mode end-to-end. ### Server-side: private-key `Account` Use `privateKeyToAccount` from viem to sign with a private key held by your backend. ```ts import "dotenv/config"; import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const stable = createStable({ network: Network.Mainnet, account, }); const { txHash } = await stable.transfer({ from: account.address, to: "0xRecipient", amount: 5, }); console.log(txHash); ``` ```text 0x8f3a...2d41 ``` ### Browser-side: `Transport` from a wallet Pass `custom(window.ethereum)` (or any EIP-1193 provider) as `transport`. The SDK builds the `WalletClient` and reads the signer address from the provider. ```ts import { createStable, Network } from "@stablechain/sdk"; import { custom } from "viem"; const stable = createStable({ network: Network.Mainnet, transport: custom(window.ethereum), }); const [from] = await window.ethereum.request({ method: "eth_requestAccounts" }); const { txHash } = await stable.transfer({ from, to: "0xRecipient", amount: 5, }); ``` ```text 0x8f3a...2d41 ``` :::warning `transfer`, `bridge`, and `swap` call `switchChain` to put the wallet on the right network. If the user rejects, the SDK throws `StableTransactionError` with `phase: "switch_chain"`. Catch it and surface a retry to the user. ::: ### Bring your own `WalletClient` When you already have a `WalletClient` (for example, from wagmi or a custom signer), pass it directly. It takes precedence over `account` and `transport`. ```ts import { createStable, Network } from "@stablechain/sdk"; import { createWalletClient, custom } from "viem"; import { stable as stableChain } from "viem/chains"; const walletClient = createWalletClient({ chain: stableChain, transport: custom(window.ethereum), }); const [from] = await walletClient.requestAddresses(); const stable = createStable({ network: Network.Mainnet, walletClient, }); const { txHash } = await stable.transfer({ from, to: "0xRecipient", amount: 5 }); ``` ```text 0x8f3a...2d41 ``` ### Pick a mode | **Mode** | **Use when** | | :------------- | :---------------------------------------------------------------------------- | | `account` | Backend services, scripts, agents — anywhere you hold the key. | | `transport` | Browser apps where the user signs with MetaMask or a wagmi-less custom flow. | | `walletClient` | You already have a configured `WalletClient` (wagmi, RainbowKit, ConnectKit). | ### Next recommended * [**Use with wagmi**](/en/how-to/sdk-with-wagmi) — Wire the SDK into a React app through wagmi hooks. * [**SDK reference**](/en/reference/sdk) — Every config field, method, enum, and error class. * [**SDK quickstart**](/en/tutorial/sdk-quickstart) — Run your first transfer, bridge, and swap on testnet. ## Use the SDK with wagmi `createStable` accepts a viem `WalletClient`, which is exactly what wagmi's `useWalletClient` returns. You connect the wallet through wagmi as you normally would, then memoize a `StableClient` whenever the wallet client changes. This guide assumes wagmi v2 and `@tanstack/react-query`. ### 1. Configure wagmi Add Stable to the wagmi config. viem ships chain definitions for both networks. ```ts import { http, createConfig } from "wagmi"; import { stable as stableMainnet, stableTestnet } from "viem/chains"; import { injected } from "wagmi/connectors"; export const wagmiConfig = createConfig({ chains: [stableMainnet, stableTestnet], connectors: [injected()], transports: { [stableMainnet.id]: http(), [stableTestnet.id]: http(), }, }); ``` ```text WagmiConfig { chains: [988, 2201], connectors: [injected] } ``` ### 2. Build a hook that returns a `StableClient` Memoize a `StableClient` against the current `WalletClient`. Recreate it when the wallet client identity changes. ```tsx import { useMemo } from "react"; import { useWalletClient } from "wagmi"; import { createStable, Network, type StableClient } from "@stablechain/sdk"; export function useStable(network: Network = Network.Mainnet): StableClient | null { const { data: walletClient } = useWalletClient(); return useMemo(() => { if (!walletClient) return null; return createStable({ network, walletClient }); }, [walletClient, network]); } ``` :::warning `useWalletClient()` returns `undefined` before the user connects. Always guard before calling SDK methods, or the destructured `walletClient` will be falsy and `createStable` will not have a signer. ::: ### 3. Use it in a component ```tsx import { useAccount, useChainId } from "wagmi"; import { Network } from "@stablechain/sdk"; import { useStable } from "./useStable"; export function PayButton() { const { address } = useAccount(); const chainId = useChainId(); const stable = useStable(Network.Mainnet); async function onClick() { if (!stable || !address) return; const { txHash } = await stable.transfer({ from: address, to: "0xRecipient", amount: 1, }); console.log("Sent:", txHash); } return ( ); } ``` ```text Sent: 0x8f3a...2d41 ``` ### 4. Bridge and swap from React The same `stable` instance handles bridge and swap. Fetch the quote in an effect or a `useQuery`, then execute on click. ```tsx const stable = useStable(Network.Mainnet); const onSwap = async () => { if (!stable) return; const quote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, }); const { txHash, toAmount } = await stable.swap({ fromToken: quote.fromToken, toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, quote, }); console.log({ txHash, toAmount }); }; ``` ```text { txHash: "0xabcd...", toAmount: 99.81 } ``` :::note Caching quotes with `useQuery` works well: pass `quoteSwap` / `quoteBridge` as the query function and forward the cached `quote` into `swap` / `bridge`. The SDK skips its internal quote call when one is provided. ::: ### Next recommended * [**SDK reference**](/en/reference/sdk) — Every method, config field, and error class. * [**Use with viem**](/en/how-to/sdk-with-viem) — Compare the three signing modes side-by-side. * [**SDK quickstart**](/en/tutorial/sdk-quickstart) — Run your first transfer, bridge, and swap on testnet. ## Self-hosted gas waiver Self-hosted Gas Waiver lets you operate your own waiver infrastructure instead of using the hosted Waiver Server API. You register a waiver address through on-chain governance, then broadcast wrapper transactions directly to the network. This guide covers registering a waiver address, collecting signed user transactions, constructing wrapper transactions, and broadcasting them. :::note **Concept:** For what Gas Waiver is and why it exists, see [Gas waiver](/en/explanation/gas-waiver). For the full protocol specification (wrapper transaction mechanism, authorization, policy checks, execution semantics, security model), see [Gas waiver protocol](/en/reference/gas-waiver-api). ::: For the hosted Waiver Server API integration path, see [Enable gas-free transactions](/en/how-to/integrate-gas-waiver). ### Prerequisites * A waiver address registered on-chain via validator governance. * `AllowedTarget` policy configured for your target contracts. ### Overview The self-hosted flow: 1. **Collect a signed InnerTx** from the user with `gasPrice = 0`. 2. **Construct a WrapperTx**: RLP-encode the InnerTx and wrap it in a transaction sent to the marker address. 3. **Broadcast** the WrapperTx via `eth_sendRawTransaction`. ### Step 1: Collect the user's InnerTx The user signs a transaction with `gasPrice = 0`. The `to` address and method selector must match your waiver's `AllowedTarget` policy. ```typescript // config.ts export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet MARKER_ADDRESS: "0x000000000000000000000000000000000000f333", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; ``` ```typescript // collectInnerTx.ts import { ethers } from "ethers"; import { CONFIG } from "./config"; const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ recipientAddress, ethers.parseUnits("0.01", 18) ]); const gasEstimate = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit: gasEstimate, nonce: nonce, chainId: CONFIG.CHAIN_ID, }; const signedInnerTx = await userWallet.signTransaction(innerTx); ``` ### Step 2: Construct the WrapperTx RLP-encode the signed InnerTx and wrap it in a transaction to the marker address. The `gasLimit` must cover both the inner execution and the wrapping overhead. ```typescript // constructWrapper.ts import { ethers } from "ethers"; import { CONFIG } from "./config"; const innerTxBytes = ethers.decodeRlp(signedInnerTx); const rlpEncoded = ethers.encodeRlp(innerTxBytes); const waiverNonce = await provider.getTransactionCount(waiverWallet.address); const wrapperTx = { to: CONFIG.MARKER_ADDRESS, data: rlpEncoded, value: 0, gasPrice: 0, gasLimit: (gasEstimate * 12n / 10n) * 2n, // ~2x inner gas for overhead nonce: waiverNonce, chainId: CONFIG.CHAIN_ID, }; const signedWrapperTx = await waiverWallet.signTransaction(wrapperTx); ``` :::warning Both `InnerTx.gasPrice` and `WrapperTx.gasPrice` must be `0`. `WrapperTx.value` must also be `0`. If any of these conditions are not met, validators will reject the transaction. ::: ### Step 3: Broadcast Submit the signed WrapperTx via standard JSON-RPC. ```typescript // broadcast.ts const txHash = await provider.send("eth_sendRawTransaction", [signedWrapperTx]); console.log("Wrapper tx broadcast:", txHash); const receipt = await provider.waitForTransaction(txHash); console.log("Confirmed:", receipt.status === 1); ``` ```text Wrapper tx broadcast: 0x... Confirmed: true ``` ### Key takeaways * Self-hosted waiver requires a waiver address registered through on-chain validator governance. * The WrapperTx is sent to the marker address (`0x...f333`) with the RLP-encoded InnerTx as data. * Both InnerTx and WrapperTx must have `gasPrice = 0` and `value = 0`. ### Next recommended * [**Gas waiver concept**](/en/explanation/gas-waiver) — Understand the mechanism before you run your own. * [**Gas waiver protocol**](/en/reference/gas-waiver-api) — Reference the full protocol spec for marker routing, authorization, and execution semantics. * [**Enable gas-free transactions**](/en/how-to/integrate-gas-waiver) — Use the hosted Waiver Server API instead of self-hosting. ## Subscribe and collect This guide walks through building a subscription payment system where the subscriber authorizes once and the service provider collects each billing cycle automatically via EIP-7702 account abstraction. :::note **Concept:** For the subscription model, trade-offs, and comparison to card-on-file billing, see [Subscription billing](/en/reference/subscriptions). ::: ### What you'll build A full subscription lifecycle: the subscriber delegates and subscribes once, the provider collects on schedule (second cycle shown to prove repeat behavior), and the subscriber cancels. #### Demo ```text step 1. Subscriber delegates EOA to SubscriptionManager (EIP-7702) tx: 0x7702...aaaa step 2. Subscriber registers subscription (10 USDT0 / 30 days) subscriptionId: 0xabc... nextChargeAt: 2026-05-23T12:00:00Z step 3. Provider calls collect() on day 30 collected: 10 USDT0 gas cost: ~0.000050 USDT0 nextChargeAt: 2026-06-22T12:00:00Z step 4. Provider calls collect() on day 60 collected: 10 USDT0 gas cost: ~0.000050 USDT0 nextChargeAt: 2026-07-22T12:00:00Z step 5. Subscriber cancels subscription: inactive ``` ### Overview **Subscriber:** ``` ─── Subscriber ─────────────────────────────────────── // One-time setup: delegate EOA to the subscription contract signAuthorization(delegateContract) sendTransaction({ type: 4, authorizationList: [signedAuth] }) // Subscribe: set billing terms on own EOA sendTransaction({ to: self, data: subscribe(subscriptionId, provider, amount, interval) }) // Cancel: revoke billing access at any time sendTransaction({ to: self, data: cancelSubscription(subscriptionId) }) ``` **Service provider:** ``` ─── Service Provider ──────────────────────────────── // Each billing cycle: collect payment from subscriber's EOA // The delegate contract verifies caller, billing schedule, and amount sendTransaction({ to: subscriberEOA, data: collect(subscriptionId) }) // Automate with a cron job matching the billing interval // The contract reverts if called before the interval has elapsed ``` ### Delegate contract Subscription billing works by delegating the subscriber's EOA to a contract that enforces billing terms. Through EIP-7702, the subscriber's account temporarily gains contract logic, allowing a service provider to collect payments at each billing cycle without requiring the subscriber to sign every time. You can use an existing deployed contract or deploy your own. The example below is a minimal `SubscriptionManager` contract that supports three operations: * `subscribe`: register billing terms for a `subscriptionId`. * `collect`: provider pulls the next scheduled payment for that `subscriptionId`. * `cancelSubscription`: subscriber revokes a specific subscription. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SubscriptionManager (example) /// @notice Delegate contract for EIP-7702 subscription billing. /// Runs on the subscriber's EOA via delegation. contract SubscriptionManager { struct Subscription { address provider; uint256 amount; uint256 interval; uint256 nextChargeAt; bool active; } // Keyed by subscriptionId. // Storage is already per subscriber EOA under delegation. mapping(bytes32 => Subscription) public subscriptions; IERC20 public immutable usdt0; event SubscriptionCreated( bytes32 indexed subscriptionId, address indexed provider, uint256 amount, uint256 interval, uint256 nextChargeAt ); event SubscriptionCollected( bytes32 indexed subscriptionId, address indexed provider, uint256 amount, uint256 collectedAt ); event SubscriptionCancelled(bytes32 indexed subscriptionId); constructor(address _usdt0) { usdt0 = IERC20(_usdt0); } /// @notice Register a subscription. Called by the subscriber on their own EOA. function subscribe( bytes32 subscriptionId, address provider, uint256 amount, uint256 interval ) external { require(msg.sender == address(this), "subscriber only"); require(provider != address(0), "invalid provider"); require(amount > 0, "invalid amount"); require(interval > 0, "invalid interval"); require(!subscriptions[subscriptionId].active, "already exists"); uint256 nextChargeAt = block.timestamp + interval; subscriptions[subscriptionId] = Subscription({ provider: provider, amount: amount, interval: interval, nextChargeAt: nextChargeAt, active: true }); emit SubscriptionCreated(subscriptionId, provider, amount, interval, nextChargeAt); } /// @notice Collect a payment for a specific subscription. Called by the service provider. function collect(bytes32 subscriptionId) external { Subscription storage sub = subscriptions[subscriptionId]; require(sub.active, "not active"); require(msg.sender == sub.provider, "not provider"); require(block.timestamp >= sub.nextChargeAt, "too early"); sub.nextChargeAt += sub.interval; require(usdt0.transfer(sub.provider, sub.amount), "transfer failed"); emit SubscriptionCollected(subscriptionId, sub.provider, sub.amount, block.timestamp); } /// @notice Cancel a specific subscription. Called by the subscriber. function cancelSubscription(bytes32 subscriptionId) external { require(msg.sender == address(this), "subscriber only"); require(subscriptions[subscriptionId].active, "not active"); delete subscriptions[subscriptionId]; emit SubscriptionCancelled(subscriptionId); } } ``` :::note This contract is provided as a reference implementation for testing purposes. A delegate contract has full execution authority over the subscriber's EOA, so in production, use an audited and verified contract. For more context on EIP-7702 delegation and security, see [EIP-7702](/en/explanation/eip-7702). ::: ### Configuration ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const SUBSCRIPTION_MANAGER = "0xYourDeployedSubscriptionManager"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const subscriberWallet = new ethers.Wallet(process.env.SUBSCRIBER_KEY!, provider); ``` ### Step 1: Delegate the subscriber's EOA (EIP-7702) The subscriber signs an EIP-7702 authorization to delegate their EOA to the `SubscriptionManager`. After this, the subscriber's EOA executes the delegate contract's logic. ```typescript // delegate.ts import { subscriberWallet, provider, CHAIN_ID, SUBSCRIPTION_MANAGER } from "./config"; const authorization = { chainId: CHAIN_ID, address: SUBSCRIPTION_MANAGER, nonce: await provider.getTransactionCount(subscriberWallet.address), }; const signedAuth = await subscriberWallet.signAuthorization(authorization); const tx = await subscriberWallet.sendTransaction({ type: 4, to: subscriberWallet.address, authorizationList: [signedAuth], maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Delegation tx:", receipt.hash); ``` ```bash npx tsx delegate.ts ``` ```text Delegation tx: 0x7702...aaaa ``` ### Step 2: Register a subscription (subscriber) The subscriber calls `subscribe()` on their own EOA. Since the EOA is delegated, this executes `SubscriptionManager.subscribe`. ```typescript // subscribe.ts import { ethers } from "ethers"; import { subscriberWallet } from "./config"; const subscriptionManager = new ethers.Interface([ "function subscribe(bytes32 subscriptionId, address provider, uint256 amount, uint256 interval)", ]); const serviceProvider = "0xServiceProviderAddress"; const monthlyAmount = ethers.parseUnits("10", 6); // 10 USDT0 const interval = 30 * 24 * 60 * 60; // 30 days in seconds // Derive a unique subscriptionId from provider + plan name + local nonce const subscriptionId = ethers.solidityPackedKeccak256( ["address", "string", "uint256"], [serviceProvider, "pro-monthly", 1] ); const tx = await subscriberWallet.sendTransaction({ to: subscriberWallet.address, // call self (delegate code executes) data: subscriptionManager.encodeFunctionData("subscribe", [ subscriptionId, serviceProvider, monthlyAmount, interval, ]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Subscription registered, tx:", receipt.hash); console.log("Subscription ID:", subscriptionId); ``` ```bash npx tsx subscribe.ts ``` ```text Subscription registered, tx: 0xabcd...1234 Subscription ID: 0xfedc...9876 ``` ### Step 3: Collect a payment (service provider) Each billing cycle, the service provider calls `collect(subscriptionId)` on the subscriber's EOA. The delegate logic verifies the caller, billing schedule, and amount before transferring USDT0. ```typescript // collect.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const providerWallet = new ethers.Wallet(process.env.PROVIDER_KEY!, provider); const subscriptionManager = new ethers.Interface([ "function collect(bytes32 subscriptionId)", ]); const subscriberEOA = "0xSubscriberEOAAddress"; const subscriptionId = "0xYourSubscriptionId"; const tx = await providerWallet.sendTransaction({ to: subscriberEOA, // subscriber's EOA (runs delegate code) data: subscriptionManager.encodeFunctionData("collect", [subscriptionId]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Payment collected, tx:", receipt.hash); console.log("Gas used:", receipt.gasUsed.toString()); // In production, run this on a cron schedule matching the billing interval. // The delegate contract will revert if called before the interval has elapsed. ``` ```bash npx tsx collect.ts ``` ```text Payment collected, tx: 0x8f3a...2d41 Gas used: 52000 ``` A `collect()` call costs roughly **50k-55k gas** on Stable (21k base + 7702 delegation overhead + ERC-20 `transfer`). At a 1 gwei base fee, that's approximately `0.000050 USDT0` per billing cycle paid by the provider. ### Step 4: Cancel a subscription (subscriber) The subscriber calls `cancelSubscription(subscriptionId)` on their own EOA to revoke billing access for that specific subscription. ```typescript // cancel.ts import { ethers } from "ethers"; import { subscriberWallet } from "./config"; const subscriptionManager = new ethers.Interface([ "function cancelSubscription(bytes32 subscriptionId)", ]); const subscriptionId = "0xYourSubscriptionId"; const tx = await subscriberWallet.sendTransaction({ to: subscriberWallet.address, data: subscriptionManager.encodeFunctionData("cancelSubscription", [subscriptionId]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Subscription cancelled, tx:", receipt.hash); ``` ```bash npx tsx cancel.ts ``` ```text Subscription cancelled, tx: 0xdef0...5678 ``` ### Security model The subscriber is authorizing the delegate contract to pull funds from their EOA. Understand exactly what that authorization covers and how to limit exposure. **What the subscriber is authorizing.** By delegating to `SubscriptionManager`, the subscriber grants the contract's logic full execution authority over their EOA. The delegate can only transfer funds under the conditions coded into it: caller is the registered provider, the interval has elapsed, the amount matches the stored subscription. It cannot transfer to other addresses or bypass the interval check, because the contract code doesn't allow those actions. **Failure modes to mitigate.** * **Malicious delegate upgrade**: if the `SubscriptionManager` is a proxy whose implementation can be changed by an admin, the authorization effectively trusts that admin. Delegate only to immutable contracts or proxies with transparent, time-locked upgrades. * **Provider compromise**: if the provider's key leaks, an attacker can collect early payments up to the per-cycle amount. Subscribers should set a `spendingLimit` per subscription and monitor for unauthorized `SubscriptionCollected` events. * **Delegation replacement**: subscribing again with a different delegate wipes the subscription state. Use a modular delegate that supports multiple functions (subscription, batch payments, spending limits) under a single delegation, rather than one delegate per feature. * **Replayable signatures**: all signatures use EIP-7702 nonces tied to the subscriber's EOA, so they can't replay across chains or across delegations. **Recommended guardrails.** * Audit the delegate contract before production use. * Keep per-subscription amounts small relative to the subscriber's balance. * Monitor `SubscriptionCreated` / `SubscriptionCollected` events and surface them to the subscriber. * Offer the subscriber a clear "cancel" UI that calls `cancelSubscription(subscriptionId)` on their own EOA. ### Important considerations * **Persistent delegation**: the EIP-7702 delegation persists until the subscriber explicitly changes or clears it. No re-delegation needed each billing cycle. * **Single delegation per EOA**: if the subscriber later delegates to a different contract, the subscription delegate logic is replaced and collection fails. Use a modular delegate contract that supports multiple functions (subscriptions, batch payments, spending limits, session keys) under a single delegation. * **Schedule behavior**: this example advances `nextChargeAt` by one interval on each successful collection. If more than one billing period has elapsed, repeated `collect()` calls can catch up one period at a time. Extend the logic if your product requires a different policy. * **Use audited delegates**: only delegate to contracts that have been audited. ### Next recommended * [**Subscription billing concept**](/en/reference/subscriptions) — Understand the pull-based billing model. * [**Account abstraction**](/en/how-to/account-abstraction) — See how batch payments, spending limits, and session keys combine under one delegation. * [**EIP-7702 concept**](/en/explanation/eip-7702) — Review the delegation model that makes this possible. ## Tracking unbonding completions When an unbonding period completes, the protocol emits an `UnbondingCompleted` event through the `StableSystem` precompile (`0x0000000000000000000000000000000000009999`) via a system transaction. This lets dApps notify users and update balances in real time without running custom indexers or polling REST endpoints. :::note **Concept:** For how system transactions bridge SDK-layer events to the EVM and why it matters, see [System transactions](/en/explanation/system-transactions). ::: ### Prerequisites * Understanding of [System transactions](/en/explanation/system-transactions). * Familiarity with [Staking](/en/explanation/staking-module), specifically `undelegate` and the unbonding process. * Experience with contract event subscription and filtering using a standard web3 library (e.g. [ethers.js](https://docs.ethers.org/) v6). ### Overview * **Set up the contract instance**: create a contract instance for the StableSystem precompile. * **Handle events in your application**: subscribe to real-time events or query historical data depending on your application logic. * **Handle connection issues**: implement reconnection logic for persistent WebSocket subscriptions. ### Step 1: Set up the contract instance Create a contract instance for the `StableSystem` precompile using the `UnbondingCompleted` event ABI. ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_SYSTEM_ADDRESS = "0x0000000000000000000000000000000000009999"; export const STABLE_SYSTEM_ABI = [ "event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)", ]; export const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); export const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); ``` ### Step 2: Handle events in your application Subscribe to real-time events, query historical data, or both depending on your application logic. #### Real-time subscription Subscribe to `UnbondingCompleted` events for real-time notifications when any unbonding completes. Useful for triggering balance updates, sending notifications, or refreshing dashboard statistics. ```typescript // subscribeBasic.ts import { stableSystem } from "./config"; stableSystem.on("UnbondingCompleted", (delegator, validator, amount, event) => { console.log("Unbonding completed:"); console.log(" Delegator:", delegator); console.log(" Validator:", validator); console.log(" Amount:", ethers.formatEther(amount), "tokens"); console.log(" Block:", event.log.blockNumber); console.log(" Tx Hash:", event.log.transactionHash); }); ``` #### Filter by user To only receive events for a particular delegator address, use the indexed event parameters to create a filter. ```typescript // subscribeByUser.ts import { ethers } from "ethers"; import { stableSystem } from "./config"; const userAddress = "0xabcd..."; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { refreshUserBalance(userAddress); showNotification( `Your unbonding of ${ethers.formatEther(amount)} tokens completed!` ); }); ``` #### Filter by validator ```typescript // subscribeByValidator.ts import { stableSystem } from "./config"; const validatorAddress = "0x1234..."; const validatorFilter = stableSystem.filters.UnbondingCompleted( null, validatorAddress ); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### Historical query If your dApp needs to show a history of past unbonding completions, query historical events using event filters with block ranges. ```typescript // queryHistory.ts import { ethers } from "ethers"; import { provider, stableSystem } from "./config"; async function getUnbondingHistory( userAddress: string, fromBlock: number, toBlock: number ) { const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter(filter, fromBlock, toBlock); return events.map((event) => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash, })); } const currentBlock = await provider.getBlockNumber(); const history = await getUnbondingHistory( "0xabcd...", currentBlock - 1000, currentBlock ); ``` ### Step 3: Handle connection issues Event subscriptions rely on persistent WebSocket connections. Implement reconnection logic for production dApps. ```typescript // subscribeWithReconnection.ts import { ethers } from "ethers"; import { STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI } from "./config"; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function handleUnbonding(delegator: string, validator: string, amount: bigint) { console.log("Unbonding completed:", { delegator, validator, amount }); } function setupEventListener() { const wsProvider = new ethers.WebSocketProvider("wss://rpc.testnet.stable.xyz"); wsProvider.on("error", (error) => { console.error("Provider error:", error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, wsProvider ); stableSystem.on("UnbondingCompleted", handleUnbonding); } setupEventListener(); ``` ### Next recommended * [**System transactions concept**](/en/explanation/system-transactions) — Understand how protocol-level events reach the EVM. * [**Staking module concept**](/en/explanation/staking-module) — Review the delegation and unbonding flow. * [**Staking precompile reference**](/en/reference/staking-module-api) — Look up the methods that trigger the events tracked here. This comprehensive guide helps diagnose and resolve common issues with Stable nodes. ### Quick diagnostics #### Node health check script ```bash #!/bin/bash # quick-diagnosis.sh # Set service name (default: stable) export SERVICE_NAME=stable echo "=== Stable Node Diagnostics ===" echo "Timestamp: $(date)" echo "" # 1. Service Status echo "1. SERVICE STATUS:" systemctl status ${SERVICE_NAME} --no-pager | head -10 # 2. Sync Status echo -e "\n2. SYNC STATUS:" curl -s localhost:26657/status | jq '.result.sync_info' 2>/dev/null || echo "RPC not responding" # 3. Peer Connections echo -e "\n3. PEER COUNT:" curl -s localhost:26657/net_info | jq '.result.n_peers' 2>/dev/null || echo "Cannot get peer info" # 4. Recent Errors echo -e "\n4. RECENT ERRORS (last 20):" sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -i error | tail -20 # 5. System Resources echo -e "\n5. SYSTEM RESOURCES:" df -h / | grep -v Filesystem free -h | grep Mem top -bn1 | grep "load average" # 6. Port Status echo -e "\n6. PORT STATUS:" ss -tulpn | grep ${SERVICE_NAME} || echo "No ${SERVICE_NAME} ports found" echo -e "\n=== Diagnostics Complete ===" ``` ### Common issues and solutions #### Node won't start ##### Issue: binary not found **Error message:** ``` stabled: command not found ``` **Solution:** ```bash # Check if binary exists ls -la /usr/bin/stabled # If missing, reinstall (use arm64 if needed) wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz tar -xvzf stabled-0.7.2-linux-amd64-testnet.tar.gz sudo mv stabled /usr/bin/ sudo chmod +x /usr/bin/stabled ``` ##### Issue: permission denied **Error message:** ``` Error: open /home/user/.stabled/config/config.toml: permission denied ``` **Solution:** ```bash # Fix ownership sudo chown -R $USER:$USER ~/.stabled/ # Fix permissions chmod 700 ~/.stabled/ chmod 600 ~/.stabled/config/*.json chmod 644 ~/.stabled/config/*.toml ``` ##### Issue: address already in use **Error message:** ``` Error: listen tcp 0.0.0.0:26657: bind: address already in use ``` **Solution:** ```bash # Find process using port sudo lsof -i :26657 # Kill the process sudo kill -9 # Or change port in config sed -i 's/laddr = "tcp:\/\/0.0.0.0:26657"/laddr = "tcp:\/\/0.0.0.0:26658"/' ~/.stabled/config/config.toml ``` #### Sync issues ##### Issue: node stuck at certain height **Symptoms:** * Block height not increasing * No new blocks for > 1 minute **Solution:** ```bash # 1. Check peers curl localhost:26657/net_info | jq '.result.n_peers' # If no peers, add persistent peers echo "persistent_peers = \"5ed0f977a26ccf290e184e364fb04e268ef16430@37.187.147.27:26656,128accd3e8ee379bfdf54560c21345451c7048c7@37.187.147.22:26656\"" >> ~/.stabled/config/config.toml # 2. Reset and resync sudo systemctl stop ${SERVICE_NAME} stabled comet unsafe-reset-all --keep-addr-book sudo systemctl start ${SERVICE_NAME} # 3. Use snapshot (see Snapshots guide) ``` ##### Issue: "wrong Block.Header.AppHash" error **Error message:** ``` panic: Wrong Block.Header.AppHash. Expected XXXX, got YYYY ``` **Solution:** ```bash # This indicates state corruption - rollback to previous block sudo systemctl stop ${SERVICE_NAME} # Rollback one block stabled rollback # Restart node sudo systemctl start ${SERVICE_NAME} # If rollback doesn't work, restore from snapshot # Backup important files cp ~/.stabled/config/priv_validator_key.json ~/backup/ cp ~/.stabled/config/node_key.json ~/backup/ # Reset state stabled comet unsafe-reset-all # Restore from snapshot wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 tar -I lz4 -xf snapshot.tar.lz4 -C ~/.stabled/ sudo systemctl start ${SERVICE_NAME} ``` ##### Issue: slow sync speed **Symptoms:** * Less than 100 blocks/minute * High CPU/disk usage **Solution:** ```bash # 1. Check disk I/O iostat -x 1 5 # 2. Optimize configuration cat >> ~/.stabled/config/config.toml <> ~/.stabled/config/config.toml < db_dump.txt # 4. If repair fails, resync rm -rf ~/.stabled/data # Restore from snapshot # 5. Start node sudo systemctl start ${SERVICE_NAME} ``` ##### Issue: "too many open files" **Error message:** ``` accept: too many open files ``` **Solution:** ```bash # 1. Check current limits ulimit -n # 2. Increase limits echo "* soft nofile 65535" | sudo tee -a /etc/security/limits.conf echo "* hard nofile 65535" | sudo tee -a /etc/security/limits.conf # 3. Update systemd service sudo sed -i '/\[Service\]/a LimitNOFILE=65535' /etc/systemd/system/stabled.service # 4. Reload and restart sudo systemctl daemon-reload sudo systemctl restart ${SERVICE_NAME} ``` #### Memory issues ##### Issue: out of memory (OOM) kills **Symptoms:** ``` stabled.service: Main process exited, code=killed, status=9/KILL ``` **Solution:** ```bash # 1. Check memory usage free -h dmesg | grep -i "killed process" # 2. Add swap space sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 3. Optimize memory usage cat >> ~/.stabled/config/app.toml < $OUTPUT_DIR/system.txt df -h >> $OUTPUT_DIR/system.txt free -h >> $OUTPUT_DIR/system.txt # Service status systemctl status ${SERVICE_NAME} --no-pager > $OUTPUT_DIR/service-status.txt # Recent logs sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" > $OUTPUT_DIR/recent-logs.txt # Config files (remove sensitive data) grep -v "priv" ~/.stabled/config/config.toml > $OUTPUT_DIR/config.toml grep -v "priv" ~/.stabled/config/app.toml > $OUTPUT_DIR/app.toml # Node status curl -s localhost:26657/status > $OUTPUT_DIR/node-status.json 2>/dev/null # Create archive tar -czf $OUTPUT_DIR.tar.gz $OUTPUT_DIR/ echo "Debug info collected: $OUTPUT_DIR.tar.gz" echo "Share this file when requesting support" ``` ### Next steps * Review [Monitoring Setup](/en/how-to/monitor-node) to prevent issues * Check [Upgrade Guide](/en/how-to/upgrade-node) for version-specific issues This guide covers the upgrade process for Stable nodes, including upgrade procedures and rollback strategies. > For complete version history and upgrade details, see [Version History](/en/reference/testnet-version-history). ### Upgrade types #### Soft upgrades (non-breaking) * Can be performed at any time * Backward compatible #### Hard upgrades (breaking) * Requires upgrade at specific height * Not backward compatible #### Emergency upgrades * Critical security fixes * Immediate action required * May require chain halt ### Standard upgrade procedure #### Step 1: preparation ```bash # Check current version stabled version --long # Backup critical data cp -r ~/.stabled/config ~/stable-backup-$(date +%Y%m%d)/ # For validators only: Backup validator state cp ~/.stabled/data/priv_validator_state.json ~/stable-backup-$(date +%Y%m%d)/ # Check disk space (need 2x current data size) df -h ~/.stabled ``` #### Step 2: download new binary ```bash # For v1.2.0-rc1 upgrade (January 22, 2026) # Choose your architecture: # Linux AMD64 BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz" # OR Linux ARM64 BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz" # Download new binary wget $BINARY_URL # Extract to temporary location tar -xvzf stabled-1.2.0-rc1-linux-*.tar.gz -C /tmp/ # Verify new version /tmp/stabled version --long ``` #### Step 3: perform upgrade ##### For soft upgrades ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Backup current binary sudo mv /usr/bin/stabled /usr/bin/stabled.backup # Install new binary sudo mv /tmp/stabled /usr/bin/stabled sudo chmod +x /usr/bin/stabled # Verify installation stabled version --long # Start node sudo systemctl start ${SERVICE_NAME} # Monitor logs sudo journalctl -u ${SERVICE_NAME} -f ``` ##### For hard upgrades ```bash # Monitor for upgrade height while true; do HEIGHT=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current height: $HEIGHT" if [ $HEIGHT -ge $UPGRADE_HEIGHT ]; then break fi sleep 10 done # Node will halt automatically at upgrade height # Wait for halt message in logs sudo journalctl -u ${SERVICE_NAME} -f | grep "UPGRADE" # Once halted, perform upgrade sudo systemctl stop ${SERVICE_NAME} sudo mv /usr/bin/stabled /usr/bin/stabled.backup sudo mv /tmp/stabled /usr/bin/stabled # Start with new binary sudo systemctl start ${SERVICE_NAME} ``` #### Step 4: post-upgrade verification ```bash # Check node status curl -s localhost:26657/status | jq '.result' # Verify version curl -s localhost:26657/status | jq '.result.node_info.version' # Check peers curl -s localhost:26657/net_info | jq '.result.n_peers' # Monitor sync status watch -n 2 'curl -s localhost:26657/status | jq ".result.sync_info"' # Check for errors sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep -i error ``` ### Cosmovisor setup (automated upgrades) Cosmovisor automates the upgrade process for coordinated upgrades. #### Installation ```bash # Install cosmovisor go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest # Or download binary wget https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.0/cosmovisor-v1.7.0-linux-amd64.tar.gz tar -xzf cosmovisor-v1.7.0-linux-amd64.tar.gz sudo mv cosmovisor /usr/bin/ ``` #### Configuration ```bash # Set environment variables cat >> ~/.bashrc < /dev/null < > export.json # 3. Wait for coordinated restart instructions ``` ### Next steps * [Version History](/en/reference/testnet-version-history) - Complete upgrade history and release notes * [Monitor your node](/en/how-to/monitor-node) after upgrades * Review [Troubleshooting](/en/how-to/troubleshoot-node) for common issues ## Get testnet USDT0 Stable uses USDT0 as the gas token, so you need USDT0 in your wallet to submit transactions. There are two ways to fund a testnet wallet: the faucet for small amounts, or bridging from Ethereum Sepolia for larger amounts. ### Faucet The faucet is the fastest way to get testnet USDT0 for basic development and testing. 1. Visit [https://faucet.stable.xyz](https://faucet.stable.xyz). 2. Connect your browser wallet or paste a wallet address. 3. Select the button to receive testnet USDT0. The faucet sends 1 USDT0 per request, which is enough to deploy and interact with several contracts. #### Verify your balance Confirm the funds arrived: ```bash cast balance YOUR_ADDRESS --rpc-url https://rpc.testnet.stable.xyz ``` You should see a non-zero value. If the balance is still `0`, wait a few seconds and re-run. Stable produces a new block roughly every 0.7 seconds, so funds settle quickly. ### Bridge from Sepolia (larger amounts) If you need more USDT0 than the faucet provides, you can bridge Test USDT from Ethereum Sepolia to the Stable Testnet. #### 1. Mint Test USDT on Sepolia Call the `mint` function on the [Ethereum Sepolia Test USDT contract](https://sepolia.etherscan.io/token/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) to get the desired amount. #### 2. Bridge to Stable Testnet Send a cross-chain transfer to the LayerZero bridge contract on Ethereum Sepolia to bridge Test USDT to the Stable Testnet. For the full bridge script and contract addresses, see [Bridge USDT0 to Stable](/en/tutorial/bridge-usdt0). This guide covers various methods to synchronize your Stable node quickly using snapshots and state sync. ### Sync methods overview | Method | Sync Time | Storage Required | Use Case | | -------------------- | --------- | ---------------- | ------------------------------ | | **Pruned Snapshot** | \~10 min | \< 5 GiB | Regular full nodes | | **Archive Snapshot** | \~1 hours | \~500 GB | Archive nodes, block explorers | ### Official snapshots Stable provides official snapshots updated daily (00:00 UTC). #### Snapshot information #### Mainnet | Type | Compression | Size | URL | Update Frequency | | ----------- | ----------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------- | | **Pruned** | LZ4 | \< 5 GiB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4) | Daily | | **Archive** | ZSTD | \~300 GB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst) | Weekly | #### Testnet | Type | Compression | Size | URL | Update Frequency | | ----------- | ----------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------- | | **Pruned** | LZ4 | \< 5 GiB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4) | Daily | | **Archive** | ZSTD | \~800 GB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst) | Weekly | ### Using pruned snapshots Pruned snapshots contain recent blockchain state (last 100-1000 blocks). #### Step 1: set environment variable ```bash # Set service name (default: stable) export SERVICE_NAME=stable ``` #### Step 2: stop node service ```bash # Stop the running node sudo systemctl stop ${SERVICE_NAME} # Verify it's stopped sudo systemctl status ${SERVICE_NAME} ``` #### Step 3: backup current data (optional) ```bash # Create backup directory mkdir -p ~/stable-backup # Backup current state (optional, requires significant space) cp -r ~/.stabled/data ~/stable-backup/ ``` #### Step 4: download and extract pruned snapshot :::code-group ```bash [Mainnet] # Install dependencies sudo apt install -y wget zstd pv # Create snapshot directory mkdir -p ~/snapshot cd ~/snapshot # Download pruned snapshot with progress wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4 # Remove old data rm -rf ~/.stabled/data/* # Extract snapshot with progress indicator pv stable_pruned.tar.zst | zstd -d -c | tar -xf - -C ~/.stabled/ # Alternative extraction without pv zstd -d stable_pruned.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm stable_pruned.tar.zst ``` ```bash [Testnet] # Install dependencies sudo apt install -y wget lz4 pv # Create snapshot directory mkdir -p ~/snapshot cd ~/snapshot # Download pruned snapshot with progress wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 # Alternative: Download with resume support curl -C - -o snapshot.tar.lz4 https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 # Remove old data rm -rf ~/.stabled/data/* # Extract snapshot with progress indicator pv snapshot.tar.lz4 | tar -I lz4 -xf - -C ~/.stabled/ # Alternative extraction without pv tar -I lz4 -xvf snapshot.tar.lz4 -C ~/.stabled/ # Clean up rm snapshot.tar.lz4 ``` ::: #### Step 5: restart node ```bash # Start the node sudo systemctl start ${SERVICE_NAME} # Check status sudo systemctl status ${SERVICE_NAME} # Monitor logs sudo journalctl -u stabled -f ``` ### Using archive snapshots Archive snapshots contain complete blockchain history. #### Step 1: prepare system ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Install dependencies sudo apt install -y wget zstd pv # Check available disk space (need 2x snapshot size) df -h ~/.stabled ``` #### Step 2: download and extract archive snapshot :::code-group ```bash [Mainnet] # Create working directory mkdir -p ~/snapshot cd ~/snapshot # Download archive snapshot wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst # Clear old data rm -rf ~/.stabled/data/* # Extract with high memory for better performance pv stable_archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/ # Alternative: Standard extraction zstd -d --long=31 stable_archive.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm stable_archive.tar.zst ``` ```bash [Testnet] # Create working directory mkdir -p ~/snapshot cd ~/snapshot # Download archive snapshot wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst # Clear old data rm -rf ~/.stabled/data/* # Extract with high memory for better performance pv archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/ # Alternative: Standard extraction zstd -d --long=31 archive.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm archive.tar.zst ``` ::: #### Step 3: start node ```bash # Start service sudo systemctl start ${SERVICE_NAME} # Verify sync status curl -s localhost:26657/status | jq '.result.sync_info' ``` ### Creating your own snapshots #### Manual snapshot creation ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Create snapshot archive cd ~/.stabled tar -cf - data/ | lz4 -9 > ~/stable-snapshot-$(date +%Y%m%d).tar.lz4 # Create checksum sha256sum ~/stable-snapshot-*.tar.lz4 > checksums.txt # Restart node sudo systemctl start ${SERVICE_NAME} ``` #### Automated snapshot script ```bash #!/bin/bash # snapshot.sh - Automated snapshot creation # Configuration SNAPSHOT_DIR="/var/snapshots" STABLED_HOME="$HOME/.stabled" KEEP_DAYS=7 # Create snapshot directory mkdir -p $SNAPSHOT_DIR # Stop node sudo systemctl stop ${SERVICE_NAME} # Create snapshot SNAPSHOT_NAME="stable-snapshot-$(date +%Y%m%d-%H%M%S).tar.lz4" tar -cf - -C $STABLED_HOME data/ | lz4 -9 > $SNAPSHOT_DIR/$SNAPSHOT_NAME # Generate metadata cat > $SNAPSHOT_DIR/latest.json < { console.log("Unbonding completed for:", delegator); console.log("Amount:", ethers.formatEther(amount), "STABLE"); console.log("Tx:", event.log.transactionHash); }); console.log("Listening for UnbondingCompleted events..."); ``` ```bash npx tsx watchUnbonding.ts ``` ```text Listening for UnbondingCompleted events... Unbonding completed for: 0xabcd... Amount: 100.0 STABLE Tx: 0x12ab... ``` For the full system-transaction mechanism and the filter-by-user / historical-query patterns, see [Track unbonding completions](/en/how-to/track-unbonding). ### Per-module references Each precompile's full method list, events, and authorization rules live in its reference page. * [Bank precompile](/en/reference/bank-module-api): STABLE token transfers and supply queries. * [Distribution precompile](/en/reference/distribution-module-api): reward claims and commission. * [Staking precompile](/en/reference/staking-module-api): delegate, undelegate, redelegate, validator queries. * [System transactions](/en/reference/system-transactions-api): StableSystem event format and authorization. ### Next recommended * [**Track unbonding completions**](/en/how-to/track-unbonding) — Subscribe to the UnbondingCompleted event emitted via the StableSystem precompile. * [**System modules reference**](/en/reference/system-modules-api-overview) — Jump to the per-module ABI, method signatures, and event schemas. * [**System modules concept**](/en/explanation/system-modules-overview) — Understand why Stable exposes SDK modules through precompiles. ## Verify a smart contract Verification uploads your contract's source code to the block explorer and proves it compiles to the deployed bytecode. Once verified, users can read state, call functions, and audit the source on Stablescan without re-hosting your code. This guide walks through verifying a Foundry-deployed contract on Stable. ### Prerequisites * A contract already deployed on Stable testnet or mainnet. If you haven't deployed yet, see [Deploy a smart contract](/en/tutorial/smart-contract). * Foundry installed (`forge` available in your PATH). * The deployed contract address from your `forge create` output. ### 1. Confirm the deployed address Make sure you have the `Deployed to` address from your earlier deployment. From the [Deploy a smart contract](/en/tutorial/smart-contract) flow, this was the value printed after `forge create`. ```bash cast code 0xDeployedContractAddress --rpc-url https://rpc.testnet.stable.xyz | head -c 20 ``` ```text 0x6080604052600436... ``` A non-empty bytecode confirms the contract is deployed at that address. ### 2. Run forge verify-contract Foundry's verification flow submits your source to the Stablescan verifier. ```bash forge verify-contract \ 0xDeployedContractAddress \ src/Counter.sol:Counter \ --chain-id 2201 \ --verifier blockscout \ --verifier-url https://testnet.stablescan.xyz/api \ --watch ``` ```text Start verifying contract `0xDeployedContractAddress` deployed on 2201 Submitting verification of contract: Counter Submitted contract for verification: Response: `OK` GUID: `abc123...` URL: https://testnet.stablescan.xyz/address/0xDeployedContractAddress Contract verification status: Response: `OK` Details: `Pass - Verified` Contract successfully verified ``` `--watch` blocks until verification finishes so you don't have to poll. On mainnet, swap the chain ID to `988` and the verifier URL to `https://stablescan.xyz/api`. :::note **Constructor arguments**: If your contract takes constructor arguments, add `--constructor-args $(cast abi-encode "constructor(uint256,address)" 42 0xSomeAddress)` to the command. Without this flag, verification fails for any contract with a non-empty constructor. ::: ### 3. Confirm verification on Stablescan Open the contract page on the explorer. ```text https://testnet.stablescan.xyz/address/0xDeployedContractAddress ``` The **Contract** tab should now show source code, a green "Verified" badge, and the full ABI. Users can read state under **Read Contract** and send transactions under **Write Contract**. ### Troubleshooting * **"Bytecode does not match"**: your source compiles to different bytecode than what's deployed. Most often caused by mismatched Solidity version or optimizer settings. Pass `--compiler-version` and `--optimizer-runs` explicitly to match your `foundry.toml`. * **"GUID not found"**: the verifier hasn't registered your submission yet. Re-run with `--watch` or manually check the URL printed in the response. * **Contract uses libraries**: add `--libraries src/Lib.sol:Lib:0xDeployedLibAddress` for each linked library. ### Next recommended * [**Index contract events**](/en/how-to/index-contract) — Subscribe to on-chain events with ethers.js and build a live event stream. * [**Deploy a smart contract**](/en/tutorial/smart-contract) — Scaffold a fresh Foundry project and deploy to Stable testnet. * [**JSON-RPC reference**](/en/reference/json-rpc-api) — See which `eth_*` methods Stable supports for on-chain interactions. ## Work with USDT0 as gas On Stable, USDT0 is both the chain's native asset and an ERC-20 token. The gas token is USDT0, not a separate native asset. Standard Ethereum gas estimation works once you adjust three things: `maxPriorityFeePerGas` is always `0`, `baseFee` is denominated in USDT0, and the `value` field in a native transfer carries USDT0 (not ETH). This guide shows how to construct transactions correctly on Stable and what to change when porting Ethereum code. ### What changes vs. Ethereum | **Field** | **Ethereum** | **Stable** | | :-------------------------------- | :----------------- | :------------------- | | Gas token | ETH | USDT0 | | `maxPriorityFeePerGas` | Used for ordering | Ignored (set to `0`) | | `baseFeePerGas` | Denominated in ETH | Denominated in USDT0 | | `value` (native transfer) | Transfers ETH | Transfers USDT0 | | EIP-1559 transaction format | Supported | Supported | | `eth_estimateGas`, `eth_gasPrice` | Supported | Supported | | `eth_maxPriorityFeePerGas` | Returns a tip | Returns `0` | Because the transaction format is unchanged, existing ethers.js, viem, Hardhat, and Foundry code runs on Stable without changes. The differences are in how you *compute* gas fields, not how you encode them. ### Construct a transaction Fetch the base fee, set `maxPriorityFeePerGas` to `0`, and double the base fee as a safety margin. ```typescript // sendNative.ts import { ethers } from "ethers"; import "dotenv/config"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const maxPriorityFeePerGas = 0n; // always 0 on Stable const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // 2x headroom const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: ethers.parseEther("0.001"), // 0.001 USDT0, 18 decimals maxFeePerGas, maxPriorityFeePerGas, }); const receipt = await tx.wait(1); console.log("Tx:", receipt!.hash); console.log("Gas used:", receipt!.gasUsed.toString()); console.log("Effective gas price:", receipt!.gasPrice.toString(), "(USDT0 wei-equivalent)"); ``` ```bash npx tsx sendNative.ts ``` ```text Tx: 0x8f3a...2d41 Gas used: 21000 Effective gas price: 1000000000 (USDT0 wei-equivalent) ``` The effective gas price is a USDT0-denominated value. At `1 gwei`, a 21,000-gas native transfer costs approximately `0.000021` USDT0. ### Estimate gas cost in USDT0 `eth_estimateGas` and `eth_gasPrice` behave identically to Ethereum. The result is already in USDT0 because that is the gas token. ```typescript // estimate.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const gasPrice = await provider.send("eth_gasPrice", []); const gasEstimate = await provider.estimateGas({ to: "0xContractAddress", data: "0x...", }); const feeInUSDT0 = BigInt(gasPrice) * gasEstimate; console.log("Estimated fee:", ethers.formatEther(feeInUSDT0), "USDT0"); ``` ```bash npx tsx estimate.ts ``` ```text Estimated fee: 0.000021 USDT0 ``` :::warning `eth_maxPriorityFeePerGas` always returns `0` on Stable. If your wallet or SDK adds the RPC-returned priority fee on top of the base fee, it still works, but fee UIs that display a separate tip will show `0` and should be hidden. ::: ### Tooling configuration * **Hardhat / Foundry**: no special configuration needed. Standard EVM settings work. If your config explicitly sets a priority fee, set it to `0`. * **Wallets**: hide or disable the priority tip input field. Displaying it is misleading because the value has no effect on ordering or inclusion. * **Monitoring**: fee analytics dashboards should not chart priority fees. They are always zero on Stable. ### Common mistakes when porting from Ethereum * **Applying an ETH-denominated tip**: copying a priority-fee constant from Ethereum doesn't produce faster inclusion. Stable orders transactions by base fee only. * **Treating `value` as ETH**: a native transfer's `value` is USDT0. Don't convert it through ETH/USD prices. * **Hard-coding a fee cap**: set `maxFeePerGas` from the live `baseFeePerGas` (e.g., `baseFee * 2`) rather than a fixed value, so transactions don't stall when the base fee rises. ### Next recommended * [**Gas pricing reference**](/en/reference/gas-pricing-api) — Full base-fee model, EIP-1559 format, and `eth_*` method behavior. * [**Zero gas transactions**](/en/how-to/zero-gas-transactions) — Let an application cover gas via the Gas Waiver. * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Balance reconciliation and contract design with USDT0's dual role. ## Zero gas transactions Gas Waiver lets an application cover gas on behalf of a user. The user signs a transaction with `gasPrice = 0`, a governance-registered waiver wraps it, and validators execute the call at zero cost to the user. This guide walks through a qualifying transfer, shows how to verify gas was waived, and explains what the waiver does and doesn't cover. :::note **Concept**: For the wrapper transaction mechanism, authorization model, and security guarantees, see [Gas waiver](/en/explanation/gas-waiver) and the [Gas waiver protocol reference](/en/reference/gas-waiver-api). ::: ### What you'll build A two-script flow that submits a USDT0 transfer through the hosted Waiver Server, fetches the receipt, and confirms `gasPrice = 0`. #### Demo ```text step 1. Connect wallet, balance displayed as 0.01 USDT0 step 2. Send transaction via Gas Waiver → [Run] step 3. Result tx: 0x8f3a...2d41 Gas fee paid by you: 0.000000 USDT0 Balance after: 0.01 USDT0 ``` ### When the waiver applies A transaction qualifies when all of these hold: * The user signs the inner transaction with `gasPrice = 0`. * The submitter is a governance-registered waiver address. * The target `to` address and method selector are on the waiver's `AllowedTarget` policy. * The wrapper is sent to the marker address `0x000000000000000000000000000000000000f333` with `value = 0` and `gasPrice = 0`. If any of these fails, validators reject the wrapper without executing the inner call. Contract calls not listed in `AllowedTarget` are not covered. Arbitrary self-serve waivers are not possible; every waiver must be registered through validator governance. ### Prerequisites * An API key for the Waiver Server, issued by the Stable team. * The target contract address and method selector registered on the waiver's `AllowedTarget` policy. * A user wallet on testnet with no USDT0 required for gas. ### Step 1: sign a qualifying InnerTx The user signs a standard transaction with `gasPrice = 0`. In this example the call is a USDT0 `transfer`, which is a common `AllowedTarget` for application-covered gas flows. ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet WAIVER_SERVER: "https://waiver.testnet.stable.xyz", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; export const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); export const userWallet = new ethers.Wallet(process.env.USER_PRIVATE_KEY!, provider); ``` ```typescript // signInner.ts import { ethers } from "ethers"; import { CONFIG, provider, userWallet } from "./config"; const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ "0xRecipientAddress", ethers.parseUnits("0.001", 18), ]); const gasLimit = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit, nonce, chainId: CONFIG.CHAIN_ID, }; export const signedInnerTx = await userWallet.signTransaction(innerTx); console.log("Signed InnerTx:", signedInnerTx); ``` ```bash npx tsx signInner.ts ``` ```text Signed InnerTx: 0xf8a8...c1 ``` :::warning `gasPrice` must be `0`. A non-zero value causes the waiver server to reject the submission and validators to reject the wrapper. ::: ### Step 2: submit through the Waiver Server The Waiver Server wraps the signed inner transaction and broadcasts it. You need a server-issued API key. ```typescript // submit.ts import { CONFIG } from "./config"; import { signedInnerTx } from "./signInner"; const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.WAIVER_API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTx] }), }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let txHash = ""; while (true) { const { done, value } = await reader.read(); if (done) break; for (const line of decoder.decode(value).trim().split("\n")) { const result = JSON.parse(line); if (result.success) { txHash = result.txHash; console.log(`tx confirmed: ${txHash}`); } else { console.error(`tx failed: ${result.error.message}`); } } } export { txHash }; ``` ```bash npx tsx submit.ts ``` ```text tx confirmed: 0x8f3a...2d41 ``` ### Step 3: verify the receipt shows zero gas Fetch the receipt and confirm `effectiveGasPrice` is 0. That is the cryptographic proof that the user paid no gas. ```typescript // verify.ts import { provider } from "./config"; import { txHash } from "./submit"; const receipt = await provider.getTransactionReceipt(txHash); const gasUsed = receipt!.gasUsed; const effectiveGasPrice = receipt!.gasPrice; const totalFee = gasUsed * effectiveGasPrice; console.log("Gas used: ", gasUsed.toString()); console.log("Effective gas price:", effectiveGasPrice.toString()); console.log("Gas fee paid: ", `${totalFee.toString()} USDT0 (wei-equivalent)`); ``` ```bash npx tsx verify.ts ``` ```text Gas used: 21000 Effective gas price: 0 Gas fee paid: 0 USDT0 (wei-equivalent) ``` An `effectiveGasPrice` of `0` confirms the transaction executed under a registered waiver and the user was not charged. ### What Gas Waiver doesn't cover * **Contracts outside `AllowedTarget`**: arbitrary contract calls aren't covered. Every target is scoped per waiver through governance. * **User-submitted wrappers**: if the user submits directly to `0x...f333`, it fails. Only registered waiver addresses can wrap. * **Fee extraction**: validators don't accept a non-zero `gasPrice` on either the inner or wrapper transaction. For the full policy model and per-waiver scope rules, see [Gas waiver protocol](/en/reference/gas-waiver-api). ### Next recommended * [**Integrate the Waiver Server**](/en/how-to/integrate-gas-waiver) — Full API reference, batch submissions, error codes, and NDJSON streaming. * [**Self-hosted Gas Waiver**](/en/how-to/self-hosted-gas-waiver) — Register your own waiver address and broadcast wrappers without the hosted API. * [**Gas waiver protocol**](/en/reference/gas-waiver-api) — Read the full spec: marker routing, wrapper format, governance controls. ## Accounts guides Every guide, concept, and reference under the Accounts tab, grouped by what you're trying to do. ### Set up a wallet * [**Create a wallet**](/en/how-to/create-wallet) — Generate a new key pair or restore from a seed phrase using ethers.js or the Tether WDK. * [**Agent wallets**](/en/reference/agentic-wallets) — Self-custodied wallets for AI agents — how they differ from user wallets. ### Delegate the account (EIP-7702) * [**EIP-7702 concept**](/en/explanation/eip-7702) — What EIP-7702 enables on Stable and the security model. * [**Account abstraction how-to**](/en/how-to/account-abstraction) — Apply EIP-7702 to batch payments, spending limits, and session keys. ### Reference * [**EIP-7702 API**](/en/reference/eip-7702-api) — Type-4 transaction format and the authorization list. * [**Subscribe and collect**](/en/how-to/subscribe-and-collect) — Apply an EIP-7702 delegate to a subscription payment flow (cross-listed). ## Accounts on Stable An account on Stable is a standard Ethereum EOA that can optionally execute smart contract logic through [EIP-7702 delegation](/en/explanation/eip-7702). Users keep one address and one private key across wallets, batch payments, recurring subscriptions, and session keys. Agents run the same account model without any custodial middleware. ### What you can build * **Wallets** from a seed phrase, with native USDT0 balance queries and signed transactions. * **Batched payments**: execute multiple transfers in one atomic transaction via a delegated EOA. * **Spending limits**: enforce per-transaction or per-day caps on the EOA itself through delegate logic. * **Session keys**: grant a scoped, time-bound, budget-bound key to a dApp so users don't re-sign every action. * **Agent wallets**: fund an AI agent with a self-custodied key and let it pay for x402 services autonomously. See [Agentic wallets](/en/reference/agentic-wallets) for providers and integration patterns. ### How Stable differs * **One address for everything.** No account migration to unlock smart contract features. EIP-7702 delegates code *onto* the existing EOA. * **USDT0-only gas.** Users don't need a separate native token. A new account funds with USDT0 and can transact immediately. * **Multi-function delegate pattern.** A single delegate can combine batch, spending limits, session keys, and subscriptions so one delegation covers every feature you ship. ### Start here * [**Create a wallet**](/en/how-to/create-wallet) — Generate or restore a wallet with ethers.js or the Tether WDK. * [**Delegate with EIP-7702**](/en/how-to/account-abstraction) — Apply batch payments, spending limits, and session keys to an existing EOA. * [**Stable SDK**](/en/explanation/sdk-overview) — Use the typed client to sign and send transactions from any account. ### Next recommended * [**Accounts guide index**](/en/explanation/accounts-guides) — Jump to the full list of account guides and references. * [**EIP-7702 concept**](/en/explanation/eip-7702) — Why delegation works without account migration. * [**Subscribe and collect**](/en/how-to/subscribe-and-collect) — Apply the Accounts model to a recurring payment flow. ## Agent settlement Agent settlement is Stable's rail for machine payments. An agent holds a USDT0 balance, pays for a resource over HTTP, and the payment settles on-chain in the same request cycle. The agent spends down from one balance for both the payment and the network fee. There is no separate gas token, no sign-up, and no API key rotation. ### Why this matters for agents Agents transact differently from humans. They run continuously, make many small payments, and cannot complete sign-up flows or rotate API keys. Settlement on Stable matches that workload: * **USDT0 is the gas token and the payment token.** An agent wallet holds a single asset and spends it down for both fees and payments. * **Sub-cent, predictable fees.** Fees are denominated in dollars, so agents can budget cost per action without converting from a volatile gas asset. * **Sub-second finality.** A paid HTTP call settles inside the request lifecycle (\~700 ms block time), which makes high-frequency machine traffic viable. * **USDT distribution.** USDT is the most widely held stablecoin; Stable is the venue purpose-built for it. ### How the layers fit Two layers do different jobs and are complementary, not alternatives: * **x402** is the *payment standard*. It is an HTTP-native protocol where a server responds with `402 Payment Required`, a client signs an authorization, and a facilitator submits it. * **MPP (Machine Payments Protocol)** is the IETF-track standard that supersedes x402 with broader intents and multi-rail support; x402 is the backward-compatible subset Stable supports today. See [MPP](/en/explanation/mpp). * **Stable** is the *settlement layer*. It is where the on-chain transfer of USDT0 actually happens. A **facilitator** sits between the two: it verifies the signed payment and submits the on-chain call so the developer does not run settlement infrastructure. See [Facilitators](/en/reference/agentic-facilitators) for the providers that support Stable today. ```text agent (client) ──HTTP──▶ resource server ──signed payment──▶ facilitator ──tx──▶ Stable (returns 402) (verify + submit) (USDT0 settles) ``` ### What you can build * **Pay-per-call APIs** priced per request in USDT0, settled via x402 or MPP. * **Agent-to-agent commerce** where one agent pays another for a service over HTTP. * **Paid MCP tools** that wrap x402 endpoints so an AI client calls and pays for them through prompts. * **Autonomous procurement** against a budgeted USDT0 balance. * **Usage-based billing** that settles per request instead of per invoice. * **Agent wallets** funded with USDT0 only, no custodial middleware. ### Start here * [**Build a pay-per-call API**](/en/how-to/build-pay-per-call) — Stand up an x402-gated endpoint and settle a real USDT0 payment in the request. * [**Build an MPP endpoint on Stable**](/en/how-to/build-mpp-endpoint) — Write the three MPP custom-method hooks for USDT0 and settle on Stable. * [**Develop with AI**](/en/how-to/develop-with-ai) — Wire Docs MCP and Runtime MCP into your AI editor and paste the Stable context block. * [**Pay with an MCP server**](/en/how-to/pay-with-mcp) — Expose x402-paid APIs as MCP tools an agent can call through natural-language prompts. ### Next recommended * [**x402 in depth**](/en/explanation/x402) — Read how the HTTP payment protocol works end to end on Stable. * [**MPP**](/en/explanation/mpp) — The broader IETF-track standard that x402 belongs to. * [**Facilitators**](/en/reference/agentic-facilitators) — See which facilitators already settle USDT0 payments on Stable. ## AI and agents guides Every guide, concept, and reference under the AI/Agents tab, grouped by what you're trying to do. ### Equip an AI editor * [**Develop with AI**](/en/how-to/develop-with-ai) — Install Docs MCP, Runtime MCP, agent skills, and paste the Stable context block. * [**Create an agent wallet**](/en/how-to/create-wallet) — Self-custodied key via WDK — the foundation for agent payments. ### Monetise and consume services * [**Build a pay-per-call API**](/en/how-to/build-pay-per-call) — Price any HTTP endpoint per request in USDT0 with x402 middleware. * [**Pay with an MCP server**](/en/how-to/pay-with-mcp) — Wrap x402-paid APIs as MCP tools so an AI client calls and pays for them. ### Reference * [**Agentic facilitators**](/en/reference/agentic-facilitators) — Settlement services for agent-to-agent commerce on Stable. * [**Agent wallets**](/en/reference/agentic-wallets) — Wallet specs for autonomous agent use. ### Foundation concepts * [**x402 (HTTP-native payments)**](/en/explanation/x402) — The HTTP protocol agents use to pay per request. * [**MPP**](/en/explanation/mpp) — The broader IETF-track standard that x402 belongs to, with sessions and multi-rail support. * [**ERC-3009**](/en/explanation/erc-3009) — The signed-authorization standard x402 settles through. ## Autobahn ### Tradeoffs in BFT: latency vs. robustness Modern Byzantine Fault Tolerant (BFT) consensus protocols typically operate under the partial synchrony model. This model assumes that the network eventually stabilizes and message delays remain bounded. While practical for protocol design, real-world deployments rarely enjoy long periods of uninterrupted stability. Instead, systems frequently experience periods of synchrony followed by short disruptions such as latency spikes, node outages, or adversarial conditions. These transient disruptions are referred to as **“blips”**. Under such conditions, existing consensus protocols are forced to **choose between low latency in stable network conditions and robustness in the presence of faults.** * **Traditional view-based BFT protocols**, such as PBFT and HotStuff, are optimized for responsiveness during good intervals when the network is stable. However, they suffer from degraded performance when a blip occurs. This degradation, known as a hangover, can persist even after the network has recovered, as backlogged requests accumulate and delay subsequent transactions. * **DAG-based BFT protocols**, such as [Narwhal & Tusk](https://arxiv.org/pdf/2105.11827)/[Bullshark](https://arxiv.org/pdf/2201.05677), decouple data dissemination (DAG) from consensus (BFT) and propagate transactions asynchronously across replicas. This design enables high throughput and allows the system to continue making progress during network disruptions. However, these protocols tend to incur high latency even during good intervals due to the complexity of their asynchronous ordering mechanisms. [**Autobahn**](https://arxiv.org/pdf/2401.10369) introduces a new approach that bridges these two design philosophies. It combines the high throughput and blip tolerance of DAG-based protocols with the low latency performance of traditional view-based consensus. At the core of Autobahn is a highly parallel data dissemination layer that continuously propagates proposals at network speed, regardless of consensus progress. On top of this layer, Autobahn runs a low-latency, partially synchronous consensus protocol that commits proposals by referencing lightweight snapshots of the data layer. A defining feature of Autobahn is its ability to recover from blips without performance degradation. This property, referred to as **seamlessness**, ensures that the system resumes full throughput and low latency immediately after the network stabilizes. No costly reprocessing of backlogged transactions is required. By cleanly separating data availability from ordering and avoiding protocol-induced synchronization delays, Autobahn offers a robust yet responsive foundation for blockchain consensus in real-world conditions. ### Autobahn architecture overview Autobahn is architected around a clear separation of responsibilities between its two core layers: a **data dissemination layer** and a **consensus layer**. This decoupling is inspired by the design of DAG-based systems like Narwhal, but Autobahn enhances this structure to support seamlessness and lower latency. The data dissemination layer is responsible for broadcasting client transactions in a scalable, asynchronous manner. It allows each replica to maintain its own lane of transaction batches, which can be propagated and certified independently of the consensus state. These lanes grow continuously, even when the consensus process stalls, ensuring that the system remains responsive to clients at all times. On top of this, Autobahn runs a partially synchronous consensus layer based on a PBFT-style protocol. However, instead of reaching agreement on individual batches of transactions, consensus is reached on "tip cuts,” which are compact summaries of the latest state of all data lanes. This design allows Autobahn to commit arbitrarily large amounts of data in a single step, minimizing the impact of blips. HotStuff tightly couples data and consensus, causing stalls when a leader fails. Bullshark incurs high commit latencies due to DAG traversal and data synchronization. Autobahn provides a smoother and faster consensus experience, inheriting the parallelism of DAGs while avoiding their latency pitfalls. ### Data dissemination layer: lanes and cars ![Autobahn: Seamless high speed BFT](/images/autobahn-high-speed1.png) *Autobahn: Seamless high speed BFT* In Autobahn, each replica proposes transactions in its own independently advancing chain called a **lane**. Each data proposal in a lane is bundled with a set of acknowledgments from other replicas, forming what the authors call a "**car**" (short for Certification of Available Request). These cars act as proof of availability (PoA), ensuring that at least one correct replica holds the data and can retransmit it if needed. Cars are chained together by including a reference to the previous car in each new proposal. This structure guarantees that validating the tip of a lane implies the availability of the entire lane history. This transitive proof of availability is key to Autobahn's instant referencing. The consensus layer can refer to a tip cut (a vector of current lane heads) and know that all prior data is retrievable without performing DAG traversal or additional synchronization. Unlike typical DAG protocols, Autobahn avoids the costly reliable broadcast steps that enforce global availability and non-equivocation. Instead, it uses minimal coordination and trusts that at least one honest replica per PoA holds the data. This enables high throughput and low tail latency even under varying load or partial failures. The data layer continues progressing independently of consensus, ensuring responsiveness during blips. ### Consensus layer: low-latency agreement ![Autobahn: Seamless high speed BFT](/images/autobahn-high-speed2.png) *Autobahn: Seamless high speed BFT* The consensus layer in Autobahn builds upon classic PBFT principles but introduces key optimizations to reduce latency and support seamless recovery. Each consensus slot targets the commitment of a "**tip cut**" that captures the latest certified proposal from every replica's lane. The consensus leader proposes this cut using a two-phase commit process: Prepare and Confirm. During the Prepare phase, replicas vote on the proposed tip cut. If the leader receives enough votes quickly (a full quorum), it can enter the Fast Path and commit immediately with only 3 message delays. If not, it proceeds to the Confirm phase, collecting another quorum of acknowledgments before finalizing the commit in 6 message delays. A key innovation is the decoupling of data synchronization from consensus voting. Replicas are allowed to vote based on the certified tips alone, even if they haven’t received the full proposal data yet. This is safe because the PoA ensures retrievability. Synchronization happens in parallel and finishes before the execution stage, avoiding protocol stalls. In the event of leader failure or timeout, view changes are triggered using timeout certificates, and new leaders can resume progress efficiently. ### Key properties of Autobahn Autobahn satisfies the standard **safety** and **liveness** guarantees expected from BFT protocols. Safety ensures that no two correct replicas commit different blocks for the same slot. Liveness guarantees progress after global stabilization time (GST) as long as a correct leader is eventually selected. More importantly, Autobahn achieves **seamlessness**. It avoids protocol-induced hangovers by allowing the consensus layer to commit arbitrarily large data backlogs in constant time. Even after a blip, as soon as synchrony returns, all data proposals that were successfully disseminated can be committed immediately. This enables Autobahn to operate smoothly in environments with intermittent faults, outperforming traditional BFT protocols in both recovery time and system responsiveness. In addition, the protocol **scales horizontally**. Each replica contributes to the system's throughput via its own lane, and consensus cuts grow naturally with the number of participants. This makes Autobahn suitable for large-scale deployments requiring both high performance and robustness. ### Low latency meets high resilience Autobahn was evaluated against leading BFT protocols, particularly Bullshark and HotStuff, under both ideal and fault-injected conditions. The results demonstrate that Autobahn achieves the best of both worlds: it matches Bullshark’s throughput, processing over 230,000 transactions per second, while reducing its latency by more than 50%. Under good network conditions, Autobahn commits transactions with just 3 to 6 message delays, compared to Bullshark’s 12. This translates to commit latencies as low as 280ms in practice, versus over 590ms for Bullshark. Unlike HotStuff, which suffers from long hangovers after blips due to backlog processing delays, Autobahn commits its entire backlog in a single step as soon as the network stabilizes. In scenarios involving leader failures or partial network partitions, Autobahn demonstrates seamless recovery. It continues disseminating data during faults and quickly commits accumulated proposals once consensus resumes. These performance advantages make Autobahn a compelling choice for blockchain platforms seeking to combine low-latency responsiveness with high throughput and fault tolerance. ### Further reading For more technical deep-dives and details, refer to: * [Autobahn: Seamless high speed BFT](https://arxiv.org/pdf/2401.10369) ### Next recommended * [**Consensus**](/en/explanation/consensus) — Return to StableBFT, the consensus implementation that Autobahn evolves. * [**Finality**](/en/explanation/finality) — Use Stable's single-slot finality when building against the RPC. ## Bank module The `x/bank` module in Stable's SDK handles token balances, transfers, and supply. Its EVM surface (the **bank precompile**) wraps this module and adds ERC-20 semantics plus an authorization layer for privileged mint/burn operations. Contracts that need to move tokens on Stable call the precompile directly without deploying their own token implementation. ### What it exposes The bank precompile provides standard ERC-20 methods: * `transfer`, `balanceOf`, `totalSupply` * `approve`, `transferFrom`, `allowance`, `revoke` These work from any caller. No registration required. It also provides privileged methods: * `mint`: mints new tokens and transfers them to an account. * `burn`: destroys tokens held by an account. * `multiTransfer`: moves tokens from one sender to many recipients in a single call. Mint and burn require the caller contract to be registered on the `x/precompile` allowlist via a governance proposal. Governance-token minting is blocked outright. This keeps supply inflation gated to authorized contracts only. ### When to use it * A DeFi contract needs to move STABLE or USDT0 on behalf of users: call `transfer` or `transferFrom` directly on the precompile. * A protocol contract mints or burns tokens based on business logic: register through governance first, then call `mint` / `burn`. * A payments contract needs one-to-many disbursement: call `multiTransfer` in a single transaction instead of looping transfers. ### Where to find the ABI The full method signatures, event payloads, and authorization flow are in the [Bank precompile reference](/en/reference/bank-module-api). ### Next recommended * [**Bank precompile reference**](/en/reference/bank-module-api) — Call `transfer`, `approve`, `mint`, `burn`, and read events. * [**System modules overview**](/en/explanation/system-modules-overview) — Return to the full list of precompile-exposed modules. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the dual-role asset model the bank module manages. ## Bridge security and DVNs A LayerZero bridge is only as secure as the verification layer that confirms a message sent on one chain happened on another. That layer is a Decentralized Verifier Network (DVN). This page explains what DVNs do, how Stable configures them on its bridges, and why a compromise of any single DVN does not put Stable at risk. ### How DVNs work When a LayerZero message moves from chain A to chain B, the destination contract does not execute it until a configured set of DVNs independently attests that the message is real. Each application picks its own configuration: * **Required DVNs.** Every required DVN must sign before the message is accepted. * **Optional DVNs with an N-of-M threshold.** An optional pool can be added on top of the required set, with a threshold like 2-of-5 that must be met in addition to required signatures. * **Block confirmation depth.** The number of source-chain confirmations DVNs wait for before signing. The safety of a bridge is entirely a function of this configuration. A 1/1 setup with a single DVN as the sole verifier means any compromise of that one DVN's signing key allows an attacker to forge cross-chain messages. A 3/3 across three independent operators requires all three to be compromised simultaneously. The difference is the difference between losing a bridge to a single stolen key and surviving a targeted attack on one operator. ### Stable's configuration Stable's bridges run a **3/3 required DVN** configuration with three independent operators: **LayerZero Labs**, **Canary**, and **Horizen**. All three must sign every cross-chain message before the destination contract will execute it. There is no optional pool with a threshold; the required set is the entire verification surface. A single compromised signing key, including LayerZero's own, does nothing against this posture. Forging a message would require simultaneous compromise of all three independent operators. For DVN contract addresses, see [Bridges: Stable's DVN operators](/en/reference/bridges#stable-s-dvn-operators). ### STABLE OFT architecture The STABLE token bridges to other chains using LayerZero's Omnichain Fungible Token (OFT) standard. Two contract types are deployed: * **`StableOFTAdapter`** on Stable. The adapter locks STABLE on the home chain and emits a LayerZero message when STABLE is sent cross-chain. * **`StableOFTUpgradeable`** on each remote chain. This contract mints STABLE on the destination when the message is verified by the configured DVNs, and burns it on the return path so the home-chain supply remains canonical. For deployed addresses on each chain, see [Bridges: STABLE OFT contracts](/en/reference/bridges#stable-oft-contracts). ### Operational dependencies Stable's own bridge security is independent of upstream protocols, but cross-chain flow through Stable can still pause when partner protocols pause their own bridges. For example, when USDT0 pauses cross-chain mint and burn, USDT0 cannot move to or from Stable until USDT0 resumes. Funds within Stable continue to move freely; only the specific cross-chain action is unavailable. Application surfaces routing through partner bridges should communicate this clearly so users understand the distinction: their funds are not at risk, only that a particular cross-chain path is temporarily unavailable. ### Next recommended * [**Bridging USDT0 to Stable**](/en/explanation/usdt0-bridging) — See how USDT0 reaches Stable through the OFT Mesh and Legacy Mesh. * [**Bridge providers and addresses**](/en/reference/bridges) — Reference contract addresses, DVN operators, and supported bridge providers. * [**LayerZero DVN documentation**](https://docs.layerzero.network/v2/concepts/protocol/security-stack-dvns) — Read LayerZero's spec for required and optional DVN verification. ## Overview New to Stable? Run the [Quick start](/en/tutorial/quick-start) first. It takes five minutes and sends a testnet transaction so the rest of the tab has something to plug into. ### Explore * [**Accounts**](/en/explanation/accounts-overview) — Create wallets, delegate EOAs with EIP-7702, and scope session keys for users and agents. * [**Payments**](/en/explanation/payments-overview) — Send USDT0, build P2P and subscription flows, settle invoices with ERC-3009, and price APIs with x402. * [**Contracts**](/en/explanation/contracts-overview) — Deploy, verify, and index Solidity contracts, and call Bank / Distribution / Staking precompiles. * [**AI and agents**](/en/explanation/agent-settlement) — Wire MCP servers into AI clients and expose x402-paid tools agents can call through prompts. ### Start here * [**Quick start**](/en/tutorial/quick-start) — Connect to testnet, fund a wallet, and send your first USDT0 transaction. * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Native and ERC-20 transfers on the same balance, with TypeScript examples. * [**Deploy a smart contract**](/en/tutorial/smart-contract) — Scaffold Foundry, configure Stable, and deploy the Counter contract. * [**Stable SDK**](/en/explanation/sdk-overview) — Use the typed TypeScript client for transfer, bridge, and swap on Stable. ## Confidential transfer **Confidential Transfer** is a privacy layer on Stable that shields the **amount** of a USDT0 transfer while keeping sender and recipient addresses publicly visible. The shielded amount is readable only by the transacting parties and authorized regulatory auditors, using zero-knowledge (ZK) cryptography to prove validity without revealing the value. The feature is under development; this page describes the target model. ### The problem it solves Standard on-chain transfers are fully transparent; anyone can read the sender, recipient, and amount. For business payments, that transparency is a data-leakage problem: * A retailer paying suppliers on-chain exposes order volumes and wholesale pricing to any observer. * A treasury moving funds between accounts advertises its position sizes. * A payroll run publishes salary data to the entire network. Full opacity (Monero-style) would solve this but breaks compliance: regulators and auditors can't verify the transaction. Selective confidentiality (amounts hidden, parties auditable) is the model Stable targets. ### What stays visible, what doesn't | Field | Visible on-chain | Shielded | | :----------------- | :--------------- | :------- | | Sender address | ✓ | | | Recipient address | ✓ | | | Transfer amount | | ✓ | | Auxiliary metadata | | ✓ | The shielded amount is encrypted. Valid proofs attest that the transfer is balance-consistent (no inflation, no negative amounts) without revealing the value itself. Only the sender, recipient, and authorized regulatory auditors can decrypt the shielded value. ### How it fits the compliance model Two properties make the design auditable: * **Deterministic auditor access.** Regulatory auditors hold keys that decrypt shielded amounts for transactions in their jurisdiction. Business privacy is preserved against random observers; compliance scrutiny is not. * **Standard address transparency.** AML/KYC tooling that operates on address-level flows (sanction checks, source-of-funds analysis) works against the same public address graph as any transparent chain. ### When to use it Confidential Transfer fits any flow where the amount is commercially sensitive but the counterparties are appropriately public: * Supplier and invoice payments where order sizes reveal pricing. * Treasury operations where position sizes reveal strategy. * Payroll where individual salaries shouldn't be indexable by competitors. * Large OTC settlements where price discovery against orderbook tape is a risk. For flows that need address-level privacy as well (e.g. whistleblower donations), Confidential Transfer alone isn't sufficient. Those use cases need additional address-obscuring primitives that Stable doesn't provide. ### Status Confidential Transfer is in development. See [Roadmap](/en/explanation/technical-roadmap) for timing. The mechanism will ship as a dedicated transfer path alongside standard USDT0 transfers; existing applications that don't opt in are unaffected. ### Next recommended * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the asset model confidential transfers shield. * [**Flow of funds**](/en/explanation/flow-of-funds) — See where confidentiality fits in the end-to-end payment lifecycle. * [**Roadmap**](/en/explanation/technical-roadmap) — Track when confidential transfer ships. ## Consensus ### PoS consensus with StableBFT Stable Blockchain leverages **StableBFT**, a customized PoS consensus protocol built on CometBFT, to deliver high throughput, low latency, and strong reliability. StableBFT provides deterministic finality (blocks are final on inclusion, without forks) and Byzantine fault tolerance up to 1/3 of validators failing or acting maliciously. To further optimize consensus performance, Stable plans to implement the following improvements in the near future: * **Decoupled Transaction and Consensus Gossiping**: Separating the transaction gossiping layer from the consensus gossiping layer prevents network congestion on the transaction side from interfering with consensus communications. * **Direct Transaction Broadcasting to the Block Proposer**: In the current model, transactions propagate through peer-to-peer gossiping among nodes, creating high gossip traffic across the network. Stable aims to improve efficiency by enabling transactions to broadcast directly to the block proposer. ### Future roadmap: DAG-based consensus To significantly accelerate consensus, Stable intends to upgrade its protocol to a DAG-based design capable of delivering up to 5x speed improvements. Traditional view-based BFT protocols like PBFT and HotStuff are optimized for low latency under stable network conditions. However, they degrade significantly during disruptions, often experiencing long recovery delays after temporary faults. First-generation DAG-based engines like Narwhal and Tusk demonstrate that decoupling data dissemination from consensus ordering can eliminate single-proposer bottlenecks and also improve robustness under network instability. However, their architecture is not directly compatible with systems like CometBFT, as they diverge from conventional height-based block semantics and mempool designs. [Autobahn](/en/explanation/autobahn) offers a PBFT-on-DAG architecture that integrates more naturally with Stable’s consensus layer, while delivering low latency under normal conditions and fast recovery in the face of network faults. The Stable team maintains a close relationship with the authors of the Autobahn paper and will leverage Autobahn’s architecture to maximize the performance of StableBFT. StableBFT, built atop Autobahn, will enable: * Parallel proposal processing by eliminating the single-leader limitation. * Faster finality by separating data propagation from final ordering. * Enhanced resilience against network adversities through robust BFT mechanisms. This advanced consensus design supports much higher throughput based on the internal proof-of-concept, which has demonstrated over 200,000 TPS (Consensus only) in controlled environments. ### Next recommended * [**Autobahn**](/en/explanation/autobahn) — Read the protocol paper that underpins StableBFT's DAG-based upgrade path. * [**Execution**](/en/explanation/execution) — See how blocks move from consensus into parallel execution. * [**Finality**](/en/explanation/finality) — Apply Stable's single-slot finality when building against the RPC. ## Contracts guides Every guide, concept, and reference under the Contracts tab, grouped by what you're trying to do. ### Build and ship a contract * [**Deploy**](/en/tutorial/smart-contract) — Scaffold a Foundry project and deploy Counter to Stable testnet. * [**Verify**](/en/how-to/verify-contract) — Upload source to Stablescan so users can read and call your contract. * [**Index events**](/en/how-to/index-contract) — Build a live event stream with ethers.js, plus historical backfill. ### Call system modules * [**Use system modules**](/en/how-to/use-system-modules) — Call Bank, Distribution, and Staking precompiles from Solidity or ethers.js. * [**Track unbonding completions**](/en/how-to/track-unbonding) — Subscribe to the UnbondingCompleted event emitted via the StableSystem precompile. ### Reference * [**System modules reference**](/en/reference/system-modules-api-overview) — Precompile addresses and per-module ABI pointers. * [**JSON-RPC API**](/en/reference/json-rpc-api) — Supported `eth_*`, `net_*`, `web3_*`, and `debug_*` methods. ### Foundation concepts * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Dual-role balance, reconciliation events, and contract design rules. * [**Difference from Ethereum**](/en/explanation/ethereum-comparison) — Gas token, finality, priority tips, and EVM compatibility. ## Contracts on Stable Stable is fully EVM-compatible. Solidity, Vyper, Hardhat, Foundry, ethers.js, and viem work unchanged. Existing contracts deploy as-is once you point tooling at Stable's RPC. On top of the standard EVM, Stable exposes protocol-level modules (Bank, Distribution, Staking) as precompiled contracts at fixed addresses, so your Solidity can call into staking and reward distribution without re-implementing them. ### What you can build * **Standard application contracts** (ERC-20, ERC-721, escrows, AMMs) with any EVM toolchain. * **Verified, indexed contracts** on Stablescan with live event streams through ethers.js. * **Protocol-integrated contracts** that call Bank / Distribution / Staking precompiles from Solidity. * **System-transaction listeners** that watch for protocol-emitted events (for example, unbonding completions) through standard `eth_getLogs`. ### How Stable differs * **USDT0 is the gas token.** `maxPriorityFeePerGas` must be `0`. The `value` field in native transfers carries USDT0, not ETH. See [Work with USDT0 as gas](/en/how-to/work-with-usdt-gas). * **USDT0 has a dual role.** Contracts that hold native USDT0 can have their balance changed by ERC-20 `transferFrom` or `permit` — never mirror native balance in a `uint256`. See [USDT0 behavior on Stable](/en/explanation/usdt0-behavior). * **Precompile addresses are fixed across testnet and mainnet.** Burn them into your contract as constants. ### Start here * [**Deploy**](/en/tutorial/smart-contract) — Scaffold Foundry, configure Stable, and deploy Counter. * [**Verify**](/en/how-to/verify-contract) — Upload source to Stablescan with forge verify-contract. * [**Index**](/en/how-to/index-contract) — Subscribe to events with ethers.js and backfill historical logs. ### Next recommended * [**Contracts guide index**](/en/explanation/contracts-guides) — Full list of contract guides, precompile references, and system module ABIs. * [**Use system modules**](/en/how-to/use-system-modules) — Call Bank / Distribution / Staking from Solidity and ethers.js. * [**JSON-RPC reference**](/en/reference/json-rpc-api) — Which `eth_*` and `debug_*` methods Stable supports. ## Core concepts Four concepts are enough to start building. Each section defines the concept, shows it, and links to the full reference. ### USDT0 as gas You pay transaction fees in USDT0, the same asset you're already holding and transacting in. There's no second token to fund or manage. USDT0 is both the native gas asset (18 decimals, read via `address(x).balance`) and an ERC-20 token (6 decimals, read via `USDT0.balanceOf(x)`). Both interfaces operate on the same underlying balance, and the protocol reconciles the 12-digit precision gap automatically. ```solidity // Both read the same balance: uint256 native = address(user).balance; // 18 decimals uint256 erc20 = IERC20(USDT0).balanceOf(user); // 6 decimals ``` :::warning Balance reconciliation emits extra `Transfer` events at the reserve address `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`. Indexers that replay `Transfer` events must filter transfers to and from this address, or they will silently double-count balances. ::: Read more: [USDT0 as gas](/en/explanation/usdt-as-gas-token) · [USDT0 behavior on Stable](/en/explanation/usdt0-behavior). ### Guaranteed blockspace Stable reserves a portion of each block's capacity for pre-allocated enterprise workloads. Reserved traffic settles with predictable latency and cost even when general traffic is congested; it doesn't compete in the fee market. This behavior is transparent at the caller level. You submit transactions the normal way; allocations are applied at the protocol level for enrolled accounts. Read more: [Guaranteed blockspace](/en/explanation/guaranteed-blockspace). ### USDT transfer aggregator High-volume USDT0 transfers are batched and verified in parallel using a MapReduce-inspired pipeline. Per-account failures are isolated, so one bad transfer doesn't abort the batch. The caller-side transfer API is unchanged. You submit transfers the normal way and gain throughput without code changes. Read more: [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator). ### EVM compatibility Standard EVM tooling works unchanged. At the EVM level, three behaviors differ from Ethereum (USDT0 as gas, covered above, is the fourth). **Single-slot finality.** A transaction is final once included in a block. Blocks are produced roughly every 0.7 seconds. **No priority tips.** `maxPriorityFeePerGas` is always ignored. The effective gas price is the base fee set by the protocol. ```typescript import { ethers } from "ethers"; const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: ethers.parseEther("0.1"), maxFeePerGas: baseFee * 2n, // 2x base fee as safety margin maxPriorityFeePerGas: 0n, // always 0 on Stable }); await tx.wait(); console.log("Included at gas price:", tx.gasPrice?.toString()); ``` ```text Included at gas price: 1000000000 ``` **Dual-role USDT0, porting risks.** Contracts ported from Ethereum should not mirror native balance, should reject `address(0)` transfers, and should not rely on `EXTCODEHASH` for address-reuse detection. :::warning Porting a contract that mirrors native balance in an internal variable is unsafe on Stable. An external `USDT0.transferFrom` call can drain the contract's native balance without invoking any contract code. Always solvency-check with `address(this).balance` at the moment of transfer. ::: Read more: [Differences from Ethereum](/en/explanation/ethereum-comparison) · [Contracts on Stable](/en/explanation/contracts-overview) · [USDT0 migration checklist](/en/explanation/usdt0-behavior). ### Confidential transfer (planned) Stable has a planned feature for zero-knowledge transfers that hide amounts while staying auditable for authorized parties. It is not yet live. Read more: [Confidential transfer](/en/explanation/confidential-transfer). ### Next recommended * [**Quick start**](/en/tutorial/quick-start) — Connect to testnet and send a first transaction. * [**USDT0 behavior**](/en/explanation/usdt0-behavior) — Port a contract to Stable without hitting dual-role gotchas. * [**Gas pricing**](/en/reference/gas-pricing-api) — Construct transactions correctly on Stable's fee model. * [**Production readiness**](/en/how-to/production-readiness) — Validate an integration before shipping to mainnet. ## Overview ### Full stack core optimization Blockchain Transaction Lifecycle The lifecycle of a blockchain transaction, from submission to finalized result, passes through multiple tightly connected stages. A transaction is first submitted through the **RPC** interface, added to the **mempool**, packaged into a block, validated through **consensus**, executed by the **state machine**, and finally written to persistent storage in the **database**. Only after completing this full pipeline does the user receive a confirmed result. Improving just one stage in isolation is not enough. Any inefficiency in the pipeline can impact the overall performance of the system. This is why Stable focuses on optimizing the blockchain stack from top to bottom. The following pages describe how Stable upgrades each layer of its architecture (Consensus, Execution, Database, and RPC) to ensure reliable and high-performance transaction processing. ### Next recommended * [**Consensus**](/en/explanation/consensus) — Learn how StableBFT extends CometBFT for high throughput and low latency. * [**Execution**](/en/explanation/execution) — See how Stable EVM runs transactions in parallel with Block-STM and Optimistic Block Processing. * [**Storage (StableDB)**](/en/explanation/stable-db) — Understand how decoupled state commitment and memory-mapped storage remove the disk I/O bottleneck. * [**High performance RPC**](/en/explanation/high-performance-rpc) — Understand the split-path RPC architecture separating reads from writes. ## Distribution module The `x/distribution` module handles staking-reward accrual and withdrawal for delegators and validators. Its precompile bridges this behavior into the EVM so a Solidity contract can claim rewards, set withdraw addresses, and query outstanding rewards without interacting with the Cosmos SDK directly. ### What it exposes * **Set withdraw address**: a delegator designates which address receives their rewards. By default, rewards go to the delegator's own address; setting a withdraw address routes them elsewhere (useful for contract-managed staking). * **Withdraw delegator rewards**: claims all outstanding rewards from a single validator in one call. * **Withdraw validator commission**: a validator claims their accumulated commission from delegators' rewards. * **Query methods**: read reward balances, commission rates, and community-pool state without a transaction. ### Authorization semantics The precompile checks that the caller is the delegator (or validator) whose state is being modified. You cannot claim someone else's rewards or change their withdraw address. ### When to use it * A vault or staking aggregator claims rewards on a schedule: call `withdrawDelegatorRewards` directly. * A DAO routes staking rewards to a treasury address: set a withdraw address once, then rewards flow automatically. * A front-end displays current reward balances: use the query methods (no transaction needed). ### Where to find the ABI Full method signatures, input/output types, and emitted events are in the [Distribution precompile reference](/en/reference/distribution-module-api). ### Next recommended * [**Distribution precompile reference**](/en/reference/distribution-module-api) — Call `withdrawDelegatorRewards`, set withdraw addresses, and read reward balances. * [**Staking module**](/en/explanation/staking-module) — See how delegation (the source of these rewards) works. * [**System transactions**](/en/explanation/system-transactions) — Learn how unbonding completions reach the EVM as events. ## EIP-7702 Stable supports **EIP-7702**, which allows an EOA to **set its account code to an existing smart contract**. The EOA executes that contract's logic while keeping its original address and private key. The delegation is persistent until the EOA explicitly changes or clears it. For a full specification, see [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). ### What EIP-7702 enables on Stable EIP-7702 lets existing EOAs execute smart contract logic without account migration. In Stable's USDT-centric payment environment, this supports patterns such as: * **Batch payments**: multiple calls (e.g., paying several recipients in a payroll run) execute in a single atomic transaction. * **Spending limits**: a delegate contract enforces daily caps or per-transaction limits on the EOA. * **Session keys**: an EOA grants a dApp scoped, time-limited transaction permissions without exposing the owner's private key. :::note **Ready to implement?** See the [Account Abstraction (EIP-7702) implementation guide](/en/reference/eip-7702-api) for contract templates, authorization signing, and transaction submission. ::: ### How it works EIP-7702 introduces a new transaction type (`0x04`) that carries an `authorizationList`. Each authorization designates a smart contract whose code the EOA will execute for that transaction. The flow is: 1. **Choose or deploy a delegate contract**: a standard Solidity contract that implements the logic you want the EOA to run. You can use an existing deployed contract or deploy your own. Use an audited contract whenever possible. 2. **Sign an authorization**: the EOA owner signs a message designating the delegate contract. 3. **Submit an EIP-7702 transaction**: the transaction includes the authorization, and the EOA runs the delegate's code during execution. After submission, the EOA's account code is set to the delegate. Subsequent transactions to the EOA execute the delegate's logic until the owner clears or replaces the delegation. ### What doesn't change * **No new account needed**: users keep their existing EOA address and private key. There is no migration step. * **Existing keys still sign**: the EOA's private key signs the authorization and any follow-on transactions. EIP-7702 does not introduce a new signing scheme. * **Standard EVM execution**: the delegate runs as regular contract code. Tooling that debugs or traces contract execution works unchanged. ### Security considerations * **Delegate access is total.** The delegate contract has full execution authority over the EOA for the duration of the delegation. Treat delegate selection as a trust decision: a malicious delegate can drain assets. * **Delegation persists.** It does not expire at the end of a single transaction. The owner must explicitly clear or replace the delegation when they no longer want it. * **Gas costs are slightly higher** due to authorization processing, but this is offset when the delegate batches multiple calls. On Stable, where the base fee is 1 gwei and gas is denominated in USDT0, the additional authorization overhead stays well under a cent, comparable to a standard ERC-20 transfer in cost. ### Next recommended * [**Account Abstraction (EIP-7702)**](/en/reference/eip-7702-api) — Implement batch payments, spending limits, and session keys against a delegate contract. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the gas model that EIP-7702 transactions run on. * [**Gas waiver**](/en/explanation/gas-waiver) — Compare delegation to gas-waived flows where an application pays the user's gas instead. ## Settle with signed authorizations ERC-3009 lets a token holder authorize a transfer by signing a message. Anyone can then submit that signed authorization to execute the transfer on-chain. The sender never needs to call the contract directly. This is the settlement mechanism behind [x402](/en/explanation/x402) payments on Stable. ### What problem does it solve? #### The allowance problem The traditional ERC-20 pattern for third-party transfers is `approve` + `transferFrom`. The sender first calls `approve` to grant a spending allowance, then the third party calls `transferFrom` to move funds. This has well-known issues: * **Two transactions required**: The sender must send an on-chain `approve` transaction before any transfer can happen. This costs gas and adds latency. * **Infinite allowance risk**: To avoid repeated approval transactions, many applications request unlimited spending permission, creating a significant security risk. ERC-3009 takes a different approach. Instead of granting an allowance, the sender signs a one-time authorization for a specific transfer. No separate approval step, no lingering spending permissions. #### The sequential nonce problem ERC-2612 (`permit`) also enables signed authorizations, but it uses sequential nonces. Multiple permits carry ordering dependencies: if nonce 5 is not consumed, nonce 6 can never execute. ERC-3009 solves this with **unique nonces**. Each authorization uses a 32-byte value instead of a sequential counter. Multiple authorizations can be created and submitted independently, in any order, without depending on each other. #### Comparison | **Property** | **ERC-20** (`approve`) | **ERC-2612** (`permit`) | **ERC-3009** | | :------------------------ | :----------------------------- | :-------------------------------- | :------------------------------ | | On-chain steps | 2 (`approve` + `transferFrom`) | 1 (`transferFrom`) | 1 (`transferWithAuthorization`) | | Uses allowance model | Required (on-chain tx) | Yes (sets allowance via `permit`) | Not required (signature) | | Nonce model | Sequential | Sequential | Unique | | Concurrent authorizations | No | No | Yes | ### How it works #### transferWithAuthorization The sender signs an EIP-712 typed data message containing the transfer details. Anyone can then call `transferWithAuthorization` on the token contract with that signed message. The contract verifies the signature, checks the validity window, executes the transfer, and marks the nonce as used. The signed authorization contains: * `from`: address of the sender (the signer) * `to`: address of the recipient * `value`: transfer amount * `validAfter`: earliest time this authorization can be executed (Unix timestamp) * `validBefore`: latest time this authorization can be executed (Unix timestamp) * `nonce`: 32-byte value ensuring uniqueness The time window (`validAfter`/`validBefore`) gives the sender precise control over when the transfer can happen. An authorization can be scheduled for the future, given a deadline, or both. If the window expires before submission, the authorization becomes invalid and the funds stay with the sender. #### receiveWithAuthorization This function works identically to `transferWithAuthorization`, with one additional check: **the caller must be the recipient**. This prevents front-running attacks where a third party observes a pending authorization and submits it first to manipulate transaction ordering. This is useful in payment scenarios where the recipient (a merchant or service provider) should be the one to initiate settlement. #### cancelAuthorization The sender can revoke an unused authorization before it is executed. The sender signs an EIP-712 cancellation message, and the contract marks the nonce as used without executing the transfer. The original authorization can no longer be submitted. ### Built-in safety properties * **One-time use**: Each unique nonce can only be used once. Resubmitting the same signed authorization reverts. * **Time-bound**: The `validAfter`/`validBefore` window ensures authorizations do not remain valid indefinitely. * **Self-contained**: One signature authorizes one specific transfer to one specific recipient for one specific amount. No lingering permissions. * **Non-custodial**: The submitter never holds the sender's funds. The transfer moves directly from sender to recipient within the contract. ### ERC-3009 on Stable USDT0 on Stable natively implements ERC-3009. Any application can use `transferWithAuthorization` without deploying additional contracts or relay infrastructure. #### Single-asset settlement On Ethereum, even with ERC-3009, the submitter needs ETH to pay gas for calling `transferWithAuthorization`. The transfer itself is in USDT, but execution depends on a separate native asset. On Stable, USDT0 serves as both the payment token and the gas token. The entire payment lifecycle, from authorization to on-chain settlement, runs on a single stablecoin. No separate native asset is needed at any step. This property is what makes ERC-3009 on Stable a strong foundation for higher-level payment protocols. [x402](/en/explanation/x402) leverages this directly, using ERC-3009 as its on-chain settlement mechanism within standard HTTP communication. ### Key takeaways * ERC-3009 lets a token holder authorize a transfer by signing a message. Anyone can submit that signed authorization to execute the transfer. * It replaces the ERC-20 allowance model with one-time-use, self-contained authorizations. No `approve` step, no lingering permissions, no double-spend risk. * Unique nonces allow multiple authorizations to be created and submitted concurrently, in any order. * USDT0 on Stable natively supports ERC-3009, and because settlement can be completed using USDT0 alone, it provides a practical foundation for x402. **See also:** * [USDT as Gas](/en/explanation/usdt-as-gas-token) * [USDT0 Behavior on Stable](/en/explanation/usdt0-behavior) * [x402 (HTTP-Native Payments)](/en/explanation/x402) ## Ethereum comparison Stable is fully EVM-compatible, so most Ethereum tools, libraries, and contract patterns work without modification. The sections below walk through what stays the same and what changes when you move from Ethereum to Stable. ### What stays the same Stable maintains full compatibility with the Ethereum development ecosystem: | **Area** | **Compatibility** | | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Languages | Solidity, Vyper | | Tooling | Hardhat, Foundry | | Libraries | ethers.js, web3.js | | Contract patterns | All standard EVM conventions (ERC-20, ERC-721, ERC-1155, proxies, etc.) | | RPC interface | Most `eth_*` methods supported (`eth_call`, `eth_sendRawTransaction`, `eth_getBalance`, `eth_getLogs`, `eth_estimateGas`, etc.). For the full list, see [JSON-RPC API](/en/reference/json-rpc-api) | Existing smart contracts, deployment scripts, and frontend integrations target Stable by changing the RPC endpoint and chain ID. ### What is different Four behaviors differ from Ethereum. #### 1. Single-slot finality Ethereum requires multiple block confirmations before a transaction is considered final. Stable provides single-slot finality: a transaction is final once included in a block. For developers, this means: * Once a transaction appears in a confirmed block, its state changes are final and irreversible. * Applications can safely rely on block inclusion as confirmation of settlement. Even with deterministic finality, applications handling financially sensitive flows should: * Verify transaction success via RPC or emitted events before proceeding with dependent actions (e.g., unlocks, redemptions). * Implement retry and reconciliation logic for automation and batch operations to handle transient submission or RPC errors. #### 2. Gas token: USDT0 On Stable, transaction fees are paid in USDT0, not a volatile native token. This provides USDT-denominated, predictable low gas costs. * Users need USDT0 in their wallet to submit transactions. * The `value` field in transactions still works for sending USDT0, similar to how ETH is sent on Ethereum. * See [USDT as gas](/en/explanation/usdt-as-gas-token) for details. #### 3. No priority tips Stable uses a single-component gas model. There is no tip-based transaction ordering. * `maxPriorityFeePerGas` is ignored (always 0). * Transaction ordering is not influenced by fee bidding. * Wallets should hide or disable the priority tip input field. * See [Gas pricing](/en/explanation/gas-pricing) for details. #### 4. USDT0 dual-role behavior USDT0 functions as both the native gas token and an ERC-20 token. This introduces behavioral differences around balance semantics, allowance safety, and certain opcode assumptions. For the full details, see [USDT0 behavior on Stable](/en/explanation/usdt0-behavior). ### Quick comparison | **Parameter** | **Stable** | **Ethereum** | | :------------------------------------ | :----------------- | :------------------------ | | Gas token | USDT0 | ETH | | Finality | Single-slot | Multi-block confirmations | | Block time | \~0.7 seconds | \~12 seconds | | Priority tip (`maxPriorityFeePerGas`) | Ignored (always 0) | Used for ordering | | EIP-1559 transaction format | Supported | Supported | | EVM compatibility | Full | N/A | ### Next recommended * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the asset model that replaces ETH for gas. * [**Gas pricing**](/en/explanation/gas-pricing) — Review the single-component fee model in detail. * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Audit contracts for dual-role asset semantics, allowance safety, and `EXTCODEHASH` behavior. ## Compatibility with the Ethereum ecosystem Stable is fully compatible with the Ethereum Virtual Machine (EVM), allowing developers to use familiar tools, libraries, and contract patterns without modification. This ensures seamless migration of existing applications and straightforward onboarding for teams already building in the Ethereum ecosystem. **Key compatibility features** * **Languages**: Supports Solidity and Vyper for smart contract development. * **Tooling**: Works out of the box with standard frameworks such as Hardhat and Foundry. * **Libraries**: Fully compatible with ethers.js, web3.js, and other common JSON-RPC clients. * **Contract patterns**: Adheres to standard EVM conventions, including ERC-20 approvals, event emission, and access-control mechanisms. * **RPC interface**: Exposes the same JSON-RPC methods used by Ethereum networks, so your existing integrations and indexers function without code changes. ## Execution ### Stable EVM Stable EVM **Stable EVM** is Stable's Ethereum-compatible execution layer. Existing Ethereum tools and wallets like MetaMask interact with Stable unchanged. Stable EVM combines the EVM's developer experience with the modular infrastructure of the Stable SDK. To bridge the gap between the Stable EVM and the Stable SDK, Stable EVM introduces a set of **precompiles**. These precompiles expose native Stable SDK module functionality to EVM smart contracts, enabling them to call into the core chain logic securely and atomically. Smart contracts can then perform privileged operations such as token transfers, staking, or governance participation. ### Future roadmap 1: Optimistic parallel execution Historically, blockchain systems have relied on sequential execution, where each transaction is processed one after another to ensure deterministic state across all nodes. While this design guarantees consistency, it severely limits throughput and scalability, especially as modern blockchains aim to support tens of thousands of transactions per second. To overcome this constraint, Stable is adopting **Block-STM**, a proven parallel execution engine that enables **Optimistic Parallel Execution (OPE)**. This allows transactions to be executed in parallel while preserving determinism, significantly enhancing performance. #### How Block-STM works Block-STM uses an optimistic concurrency control mechanism: transactions are first executed in parallel under the assumption that they won’t conflict. Then, during a validation phase, any conflicts are detected and handled through re-execution. The process relies on the following five key techniques: **1. Multi-version memory structure** Block-STM stores multiple versions of each memory key: * Each transaction reads the latest version committed by prior transactions. * During execution, both reads and writes are versioned. * Later, during validation, these versions are checked for consistency to detect conflicts. **2. Read-Set / Write-Set based validation** * During execution, each transaction logs the keys and versions it reads in a Read-Set. * At the end of execution, it records its Write-Set into the multi-version memory. * During validation, if another transaction has modified any key in the Read-Set, the transaction is marked as conflicting. It is then aborted and re-executed with an incremented incarnation number. **3. Fast conflict detection with ESTIMATE markers** * When a transaction fails, its Write-Set is marked with an ESTIMATE flag. * If another transaction reads an ESTIMATE-marked value, it immediately halts and waits for re-execution (triggered by a `READ_ERROR`). * This helps reduce overhead by quickly identifying dependencies without re-executing the full transaction set. **4. Preset transaction order** * All transactions within a block are executed according to a preset, deterministic order. * Validation and commit stages also follow this same order. * This ensures that even with parallel execution, all nodes reach the same final state. **5. Collaborative scheduler** * A Collaborative Scheduler distributes tasks between execution and validation workers in a thread-safe manner. * It prioritizes lower-index transactions to accelerate early commits and minimize re-execution. * The scheduler manages transaction incarnations for repeated attempts until they commit successfully. #### Key benefits of Block-STM * **Parallelism Without Locks**: By leveraging MVCC (Multi-Version Concurrency Control), Block-STM allows multiple transactions to read and write concurrently without the need for mutex locks. Conflicts are only checked after execution, allowing maximum throughput during the initial processing phase. * **Minimal Overhead via ESTIMATE Markers**: Failed transactions flag their Write-Sets with ESTIMATE markers, signaling dependent transactions to pause early, avoiding wasted execution. This results in faster convergence on valid execution paths. * **Efficient Scheduling and Prioritized Commits**: Using the Collaborative Scheduler, the system minimizes retries by committing lower-index transactions first. This improves overall throughput and shortens execution cycles. * **Determinism and Consensus Compatibility**: Because every transaction adheres to a fixed order, even re-executed transactions ultimately commit in the same sequence. This ensures safe and deterministic state agreement across all nodes, preserving consensus integrity even in a parallelized environment. #### OPE on Stable Optimistic Parallel Execution on Stable Stable will incorporate **Optimistic Parallel Execution (OPE)** as a core feature of its execution layer, in conjunction with **Optimistic Block Processing (OBP)**. Please note that OPE and OBP are complementary but fundamentally different strategies. #### About OBP * OBP is not about parallelism, but about execution timing. * During the `ProcessProposal` stage, Stable pre-executes blocks while they are being gossiped to other nodes. * The resulting state is cached in memory and reused during `FinalizeBlock`, saving time and reducing duplicate computation. By combining OPE and OBP, Stable can minimize both execution latency and resource contention, delivering superior performance under high transaction load. #### Expected performance gains Internal benchmarks suggest that with **Block-STM-based OPE** and **StableDB** integration, Stable can achieve **at least 2x throughput improvements** in end-to-end transaction processing. ### Future roadmap 2: StableVM++ While efforts like Optimistic Parallel Execution (OPE) and Optimistic Block Processing (OBP) focus on optimizing *how multiple transactions are executed concurrently*, there’s another vital performance lever: **how efficiently each individual transaction is processed**. Stable is currently exploring alternative EVM implementations to boost execution speed. Among the candidates, **EVMONE**, a high-performance EVM written in C++, stands out as a strong contender to replace the existing Go-based EVM. This switch is projected to deliver up to a **6x increase in EVM execution performance** based on theoretical benchmarks. ### Next recommended * [**Storage (StableDB)**](/en/explanation/stable-db) — See how decoupled state commitment feeds execution without blocking on disk I/O. * [**High performance RPC**](/en/explanation/high-performance-rpc) — Understand the split-path RPC that surfaces execution results to clients. * [**Ethereum compatibility**](/en/explanation/ethereum-compatibility) — Port existing contracts using standard EVM tooling against Stable. ## Finality rules & compatibility guarantees Stable processes transactions within an EVM-based execution environment. When a block includes a transaction, the chain applies its effects to state and makes them immediately visible to applications, contracts, and indexers. #### Execution confirmation A transaction is considered **confirmed** once: * It is successfully included in a produced block * State changes (balances, storage, events) can be observed through RPC During the public testnet phase: * Treat confirmed state as valid for application logic * Use monitoring systems to track block continuity #### Settlement considerations Stable provides single-slot finality, meaning transactions are finalized as soon as they are included in a valid block. **For developers, this ensures:** * Once a transaction appears in a confirmed block, its state changes are final and irreversible. * Applications can safely rely on block inclusion as confirmation of settlement. **Even with deterministic finality, applications handling financially sensitive flows should:** * Verify transaction success via RPC or emitted events before proceeding with dependent actions (e.g., unlocks, redemptions). * Implement retry and reconciliation logic for automation and batch operations to handle transient submission or RPC errors. #### Compatibility commitments Stable intends to maintain a consistent execution surface for developers throughout testnet growth phases. **Current commitments:** * Stable will maintain published system module interfaces and execution behavior unless explicitly noted * Any potentially disruptive changes will be: * Announced in advance * Documented in the Release & Change Log * Accompanied by migration instructions when necessary Future updates will introduce: * A formal compatibility policy * Change-level classification for developer-facing features * Clear handling guidance for version transitions ## Flow of Funds Stable is the first blockchain purpose-built for stablecoin payments. The network is optimized for high-throughput, low-latency stablecoin transactions, delivering P2P payments and merchant acceptance with immediate settlement in USDT. Application-layer gas sponsorship and waivers allow providers to offer a zero-fee experience for end users, providing the feel of a mainstream payments network while abstracting away the complexity of blockchain systems. This page describes the complete lifecycle of funds on Stable: how USDT enters the network, moves between participants, and exits back to fiat rails. ### 1. Customer deposit (on-ramp) A user brings money into the network through one of three primary channels: * **Crypto transfer**: Any major cryptocurrency is bridged or converted to USDT0 on Stable. USDT0 is the omnichain standard for USDT and the primary form factor on the network. * **Fiat on-ramp**: Card, ACH, or local payment method converts fiat to USDT0, delivered directly into the user's wallet. * **CEX withdrawal**: The user withdraws USDT from a supporting centralized exchange, selecting Stable as the destination network. The exchange settles directly into the user's wallet. In all cases the end state is the same: the user's wallet holds USDT (as USDT0) directly on Stable. ### 2. P2P / merchant transfer (on-chain pay-in) Once funds are on Stable, the customer sends USDT directly to another user or merchant. Key properties of on-chain transfers: * **Instant settlement**: transfers settle on-chain immediately. * **Non-custodial**: in the case of a non-custodial wallet, no PSP or intermediary ever touches user balances between source and destination. * **Single asset**: because USDT is both the gas and settlement asset, there are no extra tokens in the flow and no hidden spreads. * **Zero-gas option**: gas waivers allow end users to move funds without needing to manage blockchain fees. See [Gas Waiver](/en/reference/gas-waiver-api) for details. ### 3. User / merchant balance Merchants receive USDT in their Stable wallet under their own direct control. Funds are held on-chain under the user or merchant's custody. These wallets can be created and managed by a payments provider on behalf of the user. ### 4. Merchant withdrawal (off-ramp / payout) When a merchant or user requests off-chain fiat settlement: 1. The provider initiates a conversion (USDT → fiat) via banking or payout rails. 2. Funds are credited to the merchant's chosen account. The provider re-enters the flow only to cash merchants out, not during intra-ecosystem transfers. Day-to-day P2P flows require no intermediation; providers participate only at deposit (USDT transfer to a merchant's account) or withdrawal (USDT → fiat). ### Cross-asset trades Stable also supports scenarios where the payer holds a non-USDT cryptocurrency. #### User trades into another cryptocurrency A user may hold or trade into another cryptocurrency (e.g., BTC or ETH) through an integrated exchange, broker, or on-chain DEX. At the point of payment the system automatically converts the selected cryptocurrency into USDT, which is then transmitted to the merchant's Stable wallet. All on-chain settlement continues to occur in USDT regardless of the user's preferred asset. #### Merchant acceptance of cryptocurrency payments Merchants do not need to accept or manage multiple cryptocurrencies directly. They are always credited with USDT in their Stable wallet, preserving a single settlement currency across the network. This design minimizes FX exposure for merchants and simplifies reconciliation and reporting. #### Provider's role in conversion The conversion logic (e.g., BTC → USDT) may be handled by an exchange partner, liquidity provider, or the payments provider's own treasury. The merchant remains insulated from volatility or liquidity risks; they only ever receive USDT. ### Next recommended * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand how USDT0 serves as both native gas and ERC-20 balance on Stable. * [**Bridging to Stable**](/en/explanation/usdt0-bridging) — See how USDT0 moves onto Stable from other chains via OFT Mesh or Legacy Mesh. * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Submit a USDT0 transfer on testnet using standard EVM tooling. ## Gas pricing Stable uses a simplified, single-component gas fee model designed to remove fee volatility and deliver predictable, low transaction costs. Transaction ordering is not influenced by tip bidding. The effective gas price is determined solely by the protocol's base fee. ### Why this model Three properties fall out of the single-component design: * **Predictable costs**: fees are based purely on base execution cost. Tip auctions don't introduce variance. * **USDT-denominated pricing**: gas is priced in USDT0, so a developer or user reasoning about cost in dollar terms doesn't have to account for native-token price fluctuations. * **Extremely low fees**: at a base fee of 1 gwei, a native USDT0 transfer (21,000 gas) costs approximately **0.0000021 USDT0**. Even complex contract interactions stay well under a cent. ### How it compares to Ethereum | **Parameter** | **Stable** | **Ethereum** | | :------------------------------------ | :----------------- | :---------------- | | Gas token | USDT0 | ETH | | Base fee | Yes | Yes | | Priority tip (`maxPriorityFeePerGas`) | Ignored (always 0) | Used for ordering | | EIP-1559 transaction format | Supported | Supported | Stable accepts EIP-1559 (Type 2) transactions, but `maxPriorityFeePerGas` is always ignored. Transaction ordering is not influenced by tip bidding. ### Implications * **Wallets** should hide or disable priority-tip input fields. Displaying them may confuse users since the value has no effect. * **Analytics dashboards** should not track priority fees. They will always be zero. * **Transaction-construction tooling** should set `maxPriorityFeePerGas` to `0` explicitly, then compute `maxFeePerGas` from the latest block's base fee with a safety margin. ### Next recommended * [**Gas pricing reference**](/en/reference/gas-pricing-api) — Construct transactions, estimate gas, and configure tooling against Stable's fee model. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — See how USDT0 serves as both native gas and ERC-20 balance. * [**Ethereum comparison**](/en/explanation/ethereum-comparison) — Review every behavior difference you'll encounter porting from Ethereum. ## Gas waiver Governance-approved addresses (called **waivers**) submit a wrapper transaction that carries the user's signed payload and executes it at `gasPrice = 0`. The user holds no USDT0 and pays no gas. Stable operates one such waiver as a hosted service; partners can also register their own waiver addresses through validator governance. ### How it works Gas Waiver uses a wrapper-transaction pattern: 1. **The user signs an `InnerTx`** with `gasPrice = 0`. The user's signature is preserved end-to-end; the waiver cannot modify the payload without invalidating it. 2. **A waiver wraps the `InnerTx` into a `WrapperTx`** sent to a protocol marker address (`0x000000000000000000000000000000000000f333`) with `value = 0`, `gasPrice = 0`, and the signed `InnerTx` as its data payload. 3. **Validators detect the marker**, check the waiver's authorization and policy constraints, and execute the inner transaction under the user's identity (`from`, `nonce`, call semantics). Gas accounting is handled inside the waiver mechanism. The user pays nothing; the wrapper pays nothing; validators absorb the cost against the per-waiver policy. ### Authorization and policy Waivers are controlled by validator governance, not application logic. Governance provides: * **Reviewable registration**: every waiver address is registered on-chain and visible in state. * **Revocation**: validators can remove a misbehaving waiver at any time. * **Scoped access via `AllowedTarget`**: each waiver is bound to a specific set of target contracts and method selectors. The protocol rejects any wrapper whose inner `to` address and method selector fall outside that scope. A valid wrapper transaction satisfies all of the following: * `WrapperTx.to == 0x000000000000000000000000000000000000f333` (the marker address). * `WrapperTx.from` is a waiver registered on-chain via governance. * `WrapperTx.gasPrice == 0` and `InnerTx.gasPrice == 0`. * `WrapperTx.value == 0`. * `InnerTx.to` and the extracted method selector are permitted by the waiver's `AllowedTarget` policy. If any condition fails, validators reject the wrapper without executing the inner transaction. ### Security model * **User signature integrity**: the user signs the `InnerTx`. The waiver cannot mutate the payload without invalidating the signature. Partners are still responsible for ensuring the user signs only the intended payload. * **On-chain authorization**: authorization lives on-chain. Only governance-registered waiver addresses can produce a valid wrapper submission, regardless of where the request originates. * **Service-availability boundary**: when partners route through Stable's hosted Waiver Server, submission availability depends on the service. The protocol-level authorization guarantees are unaffected. ### When to use Gas Waiver Gas Waiver fits any flow where the end user shouldn't have to hold USDT0 for gas: * Consumer apps onboarding users who have no stablecoin balance yet. * Agent-driven flows where the agent's wallet funds the gas. * Enterprise payment rails where the operator absorbs network costs. For flows where the user does hold USDT0 but wants to bundle multiple calls into one signed transaction, see [EIP-7702 delegation](/en/reference/eip-7702-api) instead. ### Next recommended * [**Enable gas-free transactions**](/en/how-to/integrate-gas-waiver) — Integrate the hosted Waiver Server API with API-key submission and NDJSON responses. * [**Gas waiver protocol**](/en/reference/gas-waiver-api) — Read the full protocol spec: marker routing, wrapper format, governance controls. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the gas token that the waiver covers. ## Guaranteed blockspace **Guaranteed Blockspace** is a dedicated blockspace-allocation model that reserves a fixed share of every block's capacity for enrolled enterprise partners, regardless of broader network conditions. Transactions routed through the guaranteed path execute with predictable latency and cost. Payroll, settlement, and supplier payments don't compete with public mempool traffic. :::note **Planned.** Guaranteed Blockspace is a forward-looking roadmap item. See [Roadmap](/en/explanation/technical-roadmap) for timing. ::: ### Why this matters General-purpose chains weren't designed for fee predictability under load: * **Ethereum**: on May 1, 2022, the Yuga Labs "Otherside" NFT mint pushed peak gas above 8,000 gwei and burned over $200M in fees, breaking any workload that required deterministic cost. * **Low-fee networks** like Solana and Base attract MEV and arbitrage spam, so legitimate transactions compete with bot traffic for inclusion. ![Source: MEV and the Limits of Scaling by Flashbots and Robert Miller](/images/share-of-gas.png) *Source: MEV and the Limits of Scaling by Flashbots and Robert Miller* Enterprise payment flows can't tolerate this variance. Guaranteed Blockspace addresses it directly. ### How the guarantee works The guarantee is enforced at three layers: * **Guaranteed mempool**: validators pull guaranteed transactions from a dedicated mempool, isolated from public traffic. * **Validator-level reservation**: each validator reserves a predefined portion of every block's gas capacity for the guaranteed lane. Deterministic inclusion falls out of this. * **Dedicated RPC nodes**: the Guaranteed Blockspace API routes transactions through isolated RPC endpoints, so submission latency doesn't spike with public RPC load. The result, for an enrolled partner: * **Exclusive routing path**: submissions don't compete with public mempool traffic. * **Guaranteed inclusion**: capacity is reserved in every block regardless of network congestion. * **No decentralization trade-off**: validator openness and network participation are preserved; the guarantee lives alongside the public lane, not above it. * **Reliable on-chain performance** for business-critical operations, even under load. ### Next recommended * [**Guaranteed settlement**](/en/explanation/upcoming-use-cases) — See the payment pattern that depends on guaranteed blockspace: timed DvP settlement cycles with deterministic inclusion. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the asset that flows through guaranteed blockspace. * [**Tokenomics**](/en/reference/tokenomics) — Review how STABLE staking underpins validator blockspace guarantees. ## High performance RPC In the pursuit of a high-performance blockchain, it's not enough to only optimize consensus or block production. The RPC layer is a critical component of the end-to-end user experience because it is the interface between the blockchain and its users. Stable proposes a new RPC-dedicated architecture to overcome the limitations of traditional RPC design. ### Why high-performance RPC matters #### The user's gateway to the blockchain The **Remote Procedure Call (RPC)** interface is the primary way users interact with the blockchain: * Wallets use RPC to broadcast transactions. * dApps query state via RPC to render UI with on-chain data, to prepare and simulate transactions, fetch logs and events, etc. * Explorers, indexers, and bots all rely on RPC for real-time data. Even if the blockchain can process transactions at lightning speed and produce blocks rapidly, none of it matters if users experience latency and delays due to a slow RPC. In practice, RPC is often the bottleneck in the overall user experience. Stable’s roadmap toward a high-performance chain explicitly includes **RPC optimization** as a first-class priority. ### The problem with traditional RPC architecture #### Monolithic design and resource contention Traditional RPC Architecture Traditionally, an RPC node is simply a repurposed full node with additional RPC endpoints exposed. This means: * Syncing the chain and serving RPC requests occur on the same instance. * To scale RPC, teams must spin up entire new full nodes, triggering resource-heavy operations like state sync and consensus setup. * Consensus, execution, and RPC all share the same CPU, memory, and disk. During periods of high transaction load, a busy component **starves the others**, degrading RPC performance. In addition, traditional RPC architecture treats read-heavy and write-heavy operations identically. Even though read queries (e.g., `eth_getBalance`) vastly outnumber write transactions, there is no differentiation in how they are handled. This design is inherently inefficient and non-scalable. ### The Stable RPC architecture Stable introduces a split-path RPC architecture that separates reads from writes and optimizes each independently. Stable RPC Architecture #### Core principles * Separate the RPC into efficient lightweight RPC nodes based on functionality. * Use lightweight RPCs as edge nodes to enhance scalability. * Optimize the data path of function-specific RPCs to reduce latency, offering more direct access or management through more efficient data structures #### Performance gains Internal benchmarks of the new read RPC path demonstrate: * Supports throughput of over 10,000 RPS, with end-to-end latency under 100ms in the same environment. * Linear scalability of edge nodes, with no need for full state sync or consensus overhead. Stable’s new RPC architecture results in a significantly smoother and faster user experience, even during high traffic events. ### Future work #### Optimizing EVM view calls One exciting area of ongoing research is dedicated support for EVM view operations (`eth_call`): * These do not require transaction commitment or state updates. * Execution can happen on lightweight stateless environments using only the current state snapshot. * A specialized RPC node could be designed specifically for these operations, delivering even faster response times and reducing load on primary full nodes. #### Integration of indexer directly to the node By integrating an indexer directly into the node, it becomes possible to serve the fastest possible data to dApps. * Typical architectures: Node → RPC → Indexer (e.g., The Graph) → Storage → dApp * Proposed Architecture: Node with Indexer → DB → dApp * This architecture enables much faster data delivery as the indexer is natively integrated into the node, removing the network communication steps. ### Next recommended * [**JSON-RPC API**](/en/reference/json-rpc-api) — Use the `eth_*` methods Stable exposes for contract reads, transaction submission, and log filtering. * [**Execution**](/en/explanation/execution) — See how execution feeds state to the RPC layer. * [**Storage (StableDB)**](/en/explanation/stable-db) — Review the storage layer the RPC reads query. ## Overview Stable is an EVM-compatible Layer 1 where USDT0 is the native gas token. Most Ethereum tools, libraries, and contract patterns work without modification. You connect by pointing your RPC to Stable and switching the chain ID. ### Connect and fund * [**Connect**](/en/reference/connect) — Mainnet and testnet chain IDs, RPC endpoints, block explorers. * [**Fund your testnet wallet**](/en/how-to/use-faucet) — Get testnet USDT0 via the faucet or bridge from Sepolia. * [**Stable SDK**](/en/explanation/sdk-overview) — Use the typed TypeScript client for transfer, bridge, and swap. ### Build with USDT0 * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Native and ERC-20 transfers with TypeScript examples. * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Dual-role balance reconciliation, contract design requirements, migration checklist. * [**Differences from Ethereum**](/en/explanation/ethereum-comparison) — Single-slot finality, USDT0 gas, no priority tips. * [**Zero-gas transactions**](/en/how-to/integrate-gas-waiver) — Gas Waiver integration via the Waiver Server API. ### Payments * [**ERC-3009**](/en/explanation/erc-3009) — Transfer With Authorization: the on-chain settlement primitive. * [**x402**](/en/explanation/x402) — HTTP-native payments with no accounts or API keys. * [**P2P payments**](/en/reference/p2p-payments) — Native and ERC-3009 delegated transfers. ### Ecosystem Providers and infrastructure already live on Stable: bridges, [the canonical Uniswap v3 deployment](/en/reference/dexes), oracles, RPCs, wallets, custody, and more. Browse the [Ecosystem](/en/reference/bridges) section for the full list. ## Key features Stable is a delegated Proof-of-Stake Layer 1 with single-slot finality, full EVM compatibility, and USDT0 as the native gas token. The features below are the ones that shape day-to-day integration. Each links to the page that covers it in depth. ### Protocol-level features | Feature | What it means | | :------------------------- | :--------------------------------------------------------------------------------------------------- | | **Single-slot finality** | A transaction is final once it's in a block. No multi-block confirmation wait. | | **Full EVM compatibility** | Solidity, Vyper, Foundry, Hardhat, ethers, viem, and `eth_*` RPC methods work unchanged. | | **USDT0 as gas** | One asset serves as both native balance and ERC-20. No separate gas token to hold. | | **Cross-chain bridging** | USDT0 moves onto Stable from Ethereum, Arbitrum, HyperEVM, Tron, and other chains via LayerZero OFT. | ### USDT-specific features * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — USDT0 serves as both the native gas token and an ERC-20 token on the same balance. * [**Gas waiver**](/en/explanation/gas-waiver) — Governance-authorized waivers submit wrapper transactions that execute at zero gas price on the user's behalf. * [**Guaranteed blockspace**](/en/explanation/guaranteed-blockspace) — Enterprise partners secure reserved capacity in every block for payment flows. * [**USDT transfer aggregator**](/en/explanation/usdt-transfer-aggregator) — High-volume USDT0 transfers batch into parallelized, fault-tolerant settlement bundles. * [**Confidential transfer**](/en/explanation/confidential-transfer) — Zero-knowledge cryptography shields transfer amounts while keeping parties auditable. For which upgrades are live today and which are on the roadmap, see [Roadmap](/en/explanation/technical-roadmap). ### Next recommended * [**Ethereum comparison**](/en/explanation/ethereum-comparison) — Identify what stays the same and what changes when you port from Ethereum to Stable. * [**Flow of funds**](/en/explanation/flow-of-funds) — Trace USDT from on-ramp through on-chain transfer to off-ramp settlement. * [**Architecture overview**](/en/explanation/core-optimization-overview) — Walk through the consensus, execution, database, and RPC layers that deliver these features. ## Learn ### Foundation * [**Overview**](/en/explanation/overview) — What Stable is and how to read this documentation. * [**Key features**](/en/explanation/key-features) — Headline specs: single-slot finality, USDT0 as gas, full EVM compatibility. * [**Difference from Ethereum**](/en/explanation/ethereum-comparison) — What stays the same and what changes when you port from Ethereum. * [**Core concepts**](/en/explanation/core-concepts) — USDT0 dual role, guaranteed blockspace, transfer aggregator, finality. ### USDT0 behavior * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Dual-role balance, reconciliation events, and contract design rules. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Why Stable uses USDT0 to pay for gas and what that means for fees. * [**Flow of funds**](/en/explanation/flow-of-funds) — How USDT moves end-to-end across Stable. * [**USDT0 features**](/en/explanation/usdt-features-overview) — Every USDT0-specific feature with links to each. ### Architecture * [**Technical overview**](/en/explanation/tech-overview) — Consensus, execution, database, and RPC layers in one page. * [**Core optimizations**](/en/explanation/core-optimization-overview) — The performance work behind sub-second finality. * [**Finality**](/en/explanation/finality) — Single-slot finality, reorg behavior, and what "confirmed" means. * [**Gas pricing**](/en/explanation/gas-pricing) — Base-fee-only model priced in USDT0. ### Use case narratives * [**Payments**](/en/explanation/use-case-payments) — Why Stable fits P2P, subscriptions, invoices, and pay-per-call. * [**Payroll**](/en/explanation/use-case-payroll) — Batched and scheduled payroll runs on Stable. * [**Sponsored transactions**](/en/explanation/use-case-sponsored) — Letting applications cover gas for their users. * [**Private transfers**](/en/explanation/use-case-private) — Upcoming confidential payment flows. ## MPP sessions A session is an MPP payment intent that batches many small payments into a single on-chain settlement. The client deposits funds into an escrow once, then signs cheap off-chain vouchers for each request. Only the net amount settles on-chain, which makes sub-cent per-request economics viable for streaming workloads. ### How a session works 1. **Deposit.** The client transfers a budget into a session-escrow contract on the settlement layer. The escrow holds the funds and exposes a settlement function that pays the seller and refunds the remainder. 2. **Voucher per request.** For each paid request, the client signs an off-chain voucher carrying `(sessionId, cumulativeAmount, nonce, expiry)`. The server checks that the cumulative amount is monotonically increasing and within the deposited balance. No on-chain action is needed at this step. 3. **Settle.** At the end of the session or on a configured cadence, a facilitator submits the latest voucher to the escrow. The escrow pays the seller the cumulative amount and returns the remaining balance to the client. Only this transaction touches the chain. A session is finalized when the latest voucher is settled or when the voucher expiry passes. ### When to use sessions vs. charge | **Workload** | **Best intent** | | :---------------------------------------------------------------------------------------------------------------- | :-------------- | | Pay-per-token LLM inference, pay-per-frame video, real-time data streams. Many small payments to the same seller. | Session | | One-off paid API calls, single-purchase resources, agent-to-agent commerce where each transaction stands alone. | Charge | The break-even point depends on how expensive the per-request on-chain settlement is relative to the request price. As soon as you are paying more in gas than in payment, sessions are the right pattern. ### Agent use cases * **Token-priced LLM inference.** A client streams completions and signs a voucher per token batch; the inference server settles at session end. * **Frame-priced video.** An agent consuming a generated video signs vouchers per N frames; the renderer settles when the stream closes. * **Real-time data feeds.** A subscriber pays per tick of an oracle or market-data stream, settling once per session window. ### Status on Stable Sessions require two pieces that Stable does not currently provide: 1. A session-aware escrow contract for USDT0 that holds the deposit and exposes a `settleVouchers` (or equivalent) function. 2. A facilitator that issues vouchers on the seller side and verifies them on the buyer side, batching submissions to the escrow. Until both ship, MPP sessions are not usable on Stable. For high-frequency agent payments today, the lowest-overhead pattern is the **charge** intent submitted through Stable's [Gas Waiver](/en/how-to/integrate-gas-waiver), which removes per-transaction gas cost on the seller's side and keeps the buyer's USDT0 balance as the only asset to manage. See [Build an MPP endpoint on Stable](/en/how-to/build-mpp-endpoint) for the per-request charge pattern. ### Next recommended * [**MPP concept**](/en/explanation/mpp) — Read the broader standard, including charge and subscription intents. * [**Agent settlement**](/en/explanation/agent-settlement) — See where MPP sessions would sit in the agent-payment rail on Stable. * [**Build an MPP endpoint on Stable**](/en/how-to/build-mpp-endpoint) — Use the charge intent today while sessions are forthcoming. ## Machine Payments Protocol (MPP) MPP (Machine Payments Protocol) is an open standard for paying for HTTP resources in the same request that asks for them. It extends [x402](/en/explanation/x402) with new payment intents (charge, subscription, session), multi-rail support (stablecoins, cards, Lightning), production features (idempotency, body-digest binding, expiration), and additional transports (MCP, WebSocket). The protocol is on the IETF standards track. ### MPP and x402 MPP clients are backward compatible: an MPP client can call an existing x402 server without changes. Where the two protocols differ: | **Aspect** | **x402** | **MPP** | | :------------------------ | :------------------------------------------------------------ | :------------------------------------------------------- | | Payment intents | Per-request charge | Charge, subscription, session | | Rails | Blockchain only | Stablecoins, cards, Lightning, custom | | Production features | Limited | Idempotency, body-digest binding, expiration | | Transports | HTTP | HTTP, MCP/JSON-RPC, WebSocket | | Headers (client ↔ server) | `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` | `WWW-Authenticate` / `Authorization` / `Payment-Receipt` | | Governance | Community protocol | IETF standards track | | Method authorship | Foundation-controlled | Permissionless | ### Challenge, credential, receipt MPP wraps every paid request in a three-step exchange between the client and the resource server: 1. **Challenge.** The server responds with `402 Payment Required` and a `WWW-Authenticate` header that names the supported methods, the amount, and an expiration. 2. **Credential.** The client picks a method, signs a proof of payment, and resubmits the request with an `Authorization` header carrying the serialized credential. 3. **Receipt.** The server verifies the credential, settles the payment, and returns the response with a `Payment-Receipt` header containing the settlement reference. A challenge can be cryptographically bound to the request body via a body digest, so a credential signed for one request cannot be reused on a different one. ### Payment intents **Charge.** A one-time payment for a single resource. The credential authorizes one transfer of an exact amount. **Subscription.** Recurring payments under a single scoped credential. The credential authorizes repeated charges across a billing period, with the rail enforcing renewal cadence. **Session.** Pay-as-you-go with off-chain vouchers. The client deposits funds into an escrow once, then signs cheap off-chain vouchers for each request. Only the net amount settles on-chain. See [MPP sessions](/en/explanation/mpp-sessions) for details. ### Transports MPP defines the same challenge / credential / receipt exchange over multiple transports: * **HTTP.** The default. Headers as listed above. * **MCP / JSON-RPC.** Lets an MCP server monetize individual tool calls. An AI client signs a credential before invoking the tool. * **WebSocket.** Persistent connections with in-band voucher top-ups, designed for streaming sessions. ### MPP on Stable MPP does not ship a Stable payment method. The `mppx` SDK ([wevm/mppx](https://github.com/wevm/mppx)) includes methods for Tempo and Stripe, and `mpp.dev` lists Tempo, Stripe, Lightning, Solana, Stellar, Monad, and RedotPay. Stable is not on either list today. The standard is permissionless, so you can author your own method. For USDT0 on Stable, the `verify()` hook is signature validation against ERC-3009, and settlement is delegated to an existing settlement service: an x402 facilitator like [Semantic Pay](https://docs.semanticpay.io) or [Heurist](https://docs.heurist.ai/x402-products/facilitator), or Stable's own [Gas Waiver](/en/how-to/integrate-gas-waiver). See [Build an MPP endpoint on Stable](/en/how-to/build-mpp-endpoint) for the full walkthrough. ### Next recommended * [**Build an MPP endpoint on Stable**](/en/how-to/build-mpp-endpoint) — Write the three MPP custom-method hooks for USDT0 and settle a real payment. * [**MPP sessions**](/en/explanation/mpp-sessions) — Stream micropayments with off-chain vouchers and one net settlement. * [**x402**](/en/explanation/x402) — Read the original HTTP-402 protocol that MPP generalizes. ## Overview Stable is a Layer 1 where USDT0 is the native gas token, and standard EVM tooling (Solidity, Foundry, Hardhat, ethers, viem, and the `eth_*` JSON-RPC methods) works unchanged. Point your RPC at Stable and confirm the chain ID: ```text 988 ``` make sure you have [foundry](https://www.getfoundry.sh/) installed to test the following command: ```bash cast chain-id --rpc-url https://rpc.stable.xyz ``` For the full list of endpoints (mainnet and testnet), see [Connect](/en/reference/connect). ### What to read next If you haven't sent a transaction on Stable yet, start with [Quick start](/en/tutorial/quick-start) for a quick walkthrough on testnet. Then pick the path that matches what you're building: * Wallets, delegation, and agent accounts → [Accounts](/en/explanation/accounts-overview). * Moving USDT0 or building payment flows → [Payments](/en/explanation/payments-overview). * Deploying smart contracts → [Contracts](/en/explanation/contracts-overview). * Wiring AI editors or building agent-paid services → [Agent settlement](/en/explanation/agent-settlement). * Running a full or archive node, ecosystem providers, or covering gas → [Infrastructure](/en/explanation/integrate-overview). Before you ship, [Core concepts](/en/explanation/core-concepts) covers four behaviors that differ from Ethereum (USDT0 dual role, guaranteed blockspace, transfer aggregator, EVM finality). [Production readiness](/en/how-to/production-readiness) is the mainnet-readiness checklist. ## Use cases overview Stable supports multiple payment patterns, from simple wallet-to-wallet transfers to agent-driven service payments. The use cases below cover the patterns that are production-ready today. For patterns on the horizon (guaranteed settlement, confidential payments, agent-to-agent commerce), see [Upcoming use cases](/en/explanation/upcoming-use-cases). ### Live use cases * [**P2P payments**](/en/reference/p2p-payments) — Wallet-to-wallet USDT0 transfers. Sub-second settlement, zero gas via Gas Waiver. * [**Subscription billing**](/en/reference/subscriptions) — Pull-based recurring billing via EIP-7702. Subscriber authorizes once, provider collects each cycle. * [**Invoice settlement**](/en/reference/invoices) — B2B invoice payment with deterministic nonces. On-chain settlement links automatically to the invoice. * [**Pay-per-call APIs**](/en/reference/pay-per-call) — Per-request HTTP payments via x402 middleware. No accounts, no API keys, no billing cycles. ### Shared foundations Most patterns build on the same two protocols: * **[ERC-3009](/en/explanation/erc-3009)**: signed authorizations for delegated settlement. Used by invoices, pay-per-call, and P2P application-initiated transfers. * **[x402](/en/explanation/x402)**: HTTP-native payments over standard headers. Used by pay-per-call APIs and MCP-driven payment flows. * **[EIP-7702](/en/explanation/eip-7702)**: EOA delegation for recurring authorization. Used by subscription billing. ### Next recommended * [**ERC-3009**](/en/explanation/erc-3009) — Start with the core settlement standard. * [**Upcoming use cases**](/en/explanation/upcoming-use-cases) — Preview agent-to-agent commerce, guaranteed settlement, and confidential payments. ## Payments guides Every guide, concept, and reference under the Payments tab, grouped by what you're trying to do. ### Send and transfer * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Native and ERC-20 transfers on the same balance. * [**Zero gas transactions**](/en/how-to/zero-gas-transactions) — Transfer USDT0 with the fee covered by a Gas Waiver. * [**Work with USDT0 as gas**](/en/how-to/work-with-usdt-gas) — Construct transactions correctly: priority tip 0, `value` in USDT0. * [**Bridge USDT0 to Stable**](/en/tutorial/bridge-usdt0) — Bridge from Ethereum Sepolia using LayerZero OFT. ### Build a payment flow * [**Learn P2P payments**](/en/how-to/build-p2p-payments) — Wallet + send + receive + history in one app. * [**Subscribe and collect**](/en/how-to/subscribe-and-collect) — Pull-based recurring billing via EIP-7702. * [**Paying with invoice**](/en/how-to/pay-with-invoice) — ERC-3009 with deterministic nonces for invoice settlement. * [**Build a pay-per-call API**](/en/how-to/build-pay-per-call) — Monetise HTTP endpoints with x402 middleware. ### Protocols and references * [**ERC-3009**](/en/explanation/erc-3009) — Transfer With Authorization: the signed-settlement primitive. * [**x402 (HTTP-native payments)**](/en/explanation/x402) — Server responds 402, client signs, facilitator settles on-chain. * [**P2P payments reference**](/en/reference/p2p-payments) — Model overview and comparison to traditional rails. * [**Subscriptions reference**](/en/reference/subscriptions) — Pull-based billing model and trade-offs. * [**Invoices reference**](/en/reference/invoices) — Deterministic-nonce settlement model. * [**Pay-per-call reference**](/en/reference/pay-per-call) — x402 pricing and endpoint discovery model. * [**Upcoming use cases**](/en/explanation/upcoming-use-cases) — Guaranteed settlement, confidential payments, agent-to-agent. ## Payments on Stable Stable is built around payments. USDT0 is the native asset and the gas token, so settlement and fees share one balance. Single-slot finality means a transfer clears in under a second. ERC-3009, EIP-7702, and x402 are native primitives, not workarounds — you can settle with a signature, pull from a delegated account, or charge per HTTP request without running a billing stack. ### What you can build * **P2P transfers** — native USDT0 sends with 21k gas and sub-second finality. * **Subscriptions** — pull-based recurring billing with EIP-7702 delegation. * **Invoice settlement** — ERC-3009 `transferWithAuthorization` with deterministic nonces for exact reconciliation. * **Pay-per-call APIs** — x402 middleware for per-request USDT0 payments; no API keys, no sign-ups. * **Zero-gas UX** — application-sponsored transactions via the Gas Waiver service. * **Cross-chain USDT0** — LayerZero OFT bridging from Ethereum and other networks. ### How Stable differs * **One asset for everything**: the sender doesn't hold a separate gas token. * **Native ERC-3009**: USDT0 implements `transferWithAuthorization` directly, so payments settle with a signature and no approve step. * **Deterministic finality**: a block is final the moment it's committed. No confirmation waits. * **Native x402**: the facilitator pays no gas through the Gas Waiver, so per-request settlement costs stay below a cent. ### Start here * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Native and ERC-20 transfers on the same balance. * [**Zero gas transactions**](/en/how-to/zero-gas-transactions) — Transfer USDT0 with the fee covered by a Gas Waiver. * [**Learn P2P payments**](/en/how-to/build-p2p-payments) — Build a wallet + send + receive + history app from scratch. * [**Build a pay-per-call API**](/en/how-to/build-pay-per-call) — Price HTTP endpoints per request with x402. * [**Stable SDK**](/en/explanation/sdk-overview) — Use the typed client for transfer, bridge, and swap in a few lines. ### Payment primitives * [**ERC-3009**](/en/explanation/erc-3009) — Transfer With Authorization: the settlement standard behind invoices and x402. * [**x402 (HTTP-native payments)**](/en/explanation/x402) — Server responds 402, client signs ERC-3009, facilitator settles on-chain. ### Next recommended * [**Payments guide index**](/en/explanation/payments-guides) — Every guide, concept, and reference under the Payments tab. * [**Subscribe and collect**](/en/how-to/subscribe-and-collect) — Pull-based recurring billing via EIP-7702. * [**Paying with invoice**](/en/how-to/pay-with-invoice) — ERC-3009 with deterministic nonces for exact reconciliation. ## Stable SDK `@stablechain/sdk` is the official TypeScript client for Stable. It wraps viem with a small, typed API for the operations you reach for most: transfer USDT0, bridge between chains, and swap tokens on Stable. Routing, approvals, decimals, and chain switching are handled for you. ```ts import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const stable = createStable({ network: Network.Mainnet, account: privateKeyToAccount("0x..."), }); const { txHash } = await stable.transfer({ from: "0xYourAddress", to: "0xRecipient", amount: 10, }); ``` ```text txHash: 0x8f3a...2d41 ``` ### What the SDK does * **`transfer`** — send native USDT0 or any ERC-20 on Stable. Gas is paid in USDT0 automatically. * **`quoteBridge` / `bridge`** — cross-chain transfers. LayerZero for USDT0 → USDT0, LI.FI for everything else. Route is picked for you. * **`quoteSwap` / `swap`** — same-chain token swaps via LI.FI, with ERC-20 approval handled internally. The SDK is published on npm as [`@stablechain/sdk`](https://www.npmjs.com/package/@stablechain/sdk) and requires `viem >= 2.0.0` as a peer dependency. ### When to use it (and when not to) Use the SDK when you want a typed, opinionated client that hides routing and approval boilerplate. Drop down to raw viem or ethers when you need direct control over transaction construction, custom gas strategies, or contract calls outside transfer / bridge / swap. :::note The SDK signs with any viem-compatible signer: a private-key `Account`, a browser `Transport` like `custom(window.ethereum)`, or a pre-built `WalletClient` (for example, the one returned by wagmi's `useWalletClient`). ::: ### Start here * [**Quickstart**](/en/tutorial/sdk-quickstart) — Install the SDK and run your first transfer, bridge, and swap on testnet. * [**SDK reference**](/en/reference/sdk) — Every method, config option, enum, and error class. * [**Use with viem**](/en/how-to/sdk-with-viem) — Server-side accounts, browser wallets, and bring-your-own `WalletClient`. * [**Use with wagmi**](/en/how-to/sdk-with-wagmi) — Wire the SDK into a React app with `useWalletClient` and hooks. ### Next recommended * [**Install from npm**](https://www.npmjs.com/package/@stablechain/sdk) — View the package on npmjs.com and check the latest version. * [**Connect to Stable**](/en/reference/connect) — Chain IDs, RPC endpoints, and explorers for mainnet and testnet. * [**Fund a testnet wallet**](/en/how-to/use-faucet) — Get testnet USDT0 from the faucet before running the quickstart. ## Storage (StableDB) One of the main bottlenecks in end-to-end blockchain performance is **Disk I/O**. Specifically, committing and storing state data after block execution creates the key bottleneck. Stable tackles this problem with architectural innovations such as `MemDB`, `VersionDB`, and memory-mapped storage (`mmap`) to dramatically improve throughput. ### Why disk I/O is a bottleneck #### State transition and persistence Every time a block of transactions is executed, the blockchain transitions from one state to the next. This process has two fundamental stages: 1. **State Commitment**: The new application state is committed after transaction execution. 2. **State Storage**: The committed state is persisted to disk for long-term access and historical verification. Coupled State Commitment and Storage In conventional architectures, state storage is **tightly coupled** with state commitment. This means that: * The node must wait for the new state to be fully stored on disk before proceeding with the next block's execution. * The state data is written in random disk locations that are not mapped to fixed addresses. This leads to high latency when retrieving state data during execution of subsequent transactions. Even if consensus and execution layers are heavily optimized, this serialized dependency on slow disk operations caps the achievable performance of the entire system. ### Optimizing DB operations for higher throughput To overcome these limitations, Stable proposes a two-fold architectural enhancement focused on **decoupling state operations** and **introducing memory-mapped DB optimizations**. #### 1. Decoupling state commitment and storage Decoupled State Commitment and Storage The first step is to decouple the state commitment from its storage: * After committing a new state, the node immediately proceeds to execute the next block. * The actual persistence of state to disk occurs asynchronously in the background. This separation allows execution to happen instantly and leapfrog the latency of slow disk writes, thereby eliminating blocking dependencies and eventually improving end-to-end performance. #### 2. Introducing `MemDB` and `VersionDB` via `mmap` Stable enhances this with a dual-database model powered by `mmap` (memory-mapped file access): * **MemDB (Memory DB)**: * Stores recent and active states that are frequently accessed. * Uses fixed address mapping via `mmap`, enabling fast and deterministic lookups. * Ideal for most transaction workloads which target recently modified state. * **VersionDB (Historical DB)**: * Stores older, historical states on disk. * Optimized for archival and long-range queries, not for high-frequency access. This design ensures that **hot data is served from fast, memory-resident structures**, while cold data is offloaded to slower, persistent storage. By combining `mmap` access with smart state tiering, Stable can significantly reduce the DB read/write latency during block execution. ### Expected gains and precedents This architectural optimization is not just theoretical. It is already being implemented by high-performance blockchains such as Sei and Cronos. Both have adopted similar decoupled architectures with memory-mapped DBs and have observed **up to 2x increases in overall TPS**. Stable also anticipates comparable gains, as the architecture will no longer be bottlenecked by the storage layer. Instead, consensus and execution performance can scale without being throttled by disk operations. ### Further reading For more technical deep-dives and implementation details, refer to: * [ADR-065: Cosmos Store V2 Architecture](https://docs.cosmos.network/main/build/architecture/adr-065-store-v2) * [MemIAVL: A Practical Guide](https://hackmd.io/@yihuang/rkeCvy5xh) * [Cronos MemIAVL Node Configuration](https://docs.cronos.org/for-node-hosts/running-nodes/memiavl) * [Sei’s DB Design Approach](https://4pillars.io/ko/articles/sei-db) ### Next recommended * [**High performance RPC**](/en/explanation/high-performance-rpc) — See how the RPC layer exposes state reads without contending with writes. * [**Execution**](/en/explanation/execution) — Understand how execution writes into the storage layer covered here. * [**Consensus**](/en/explanation/consensus) — Review the consensus layer that orders blocks before they reach storage. ## Staking module The `x/staking` module controls validator participation and delegation on Stable. Its precompile makes these operations callable from Solidity, so a contract can delegate STABLE, undelegate after the unbonding period, redelegate between validators, or query validator state without leaving the EVM. ### What it exposes * **Create validator**: register a new validator with description, commission rate, and initial self-delegation. * **Edit validator**: update validator metadata and commission parameters. * **Delegate**: stake STABLE with a validator. * **Undelegate**: begin unbonding from a validator (the tokens become available after the unbonding period). * **Redelegate**: move stake between validators without unbonding. * **Cancel unbonding delegation**: reverse an in-progress unbonding before the period completes. * **Query methods**: read validator sets, delegation records, unbonding records, and parameters. ### Authorization semantics The precompile performs two checks: 1. The bond denom (staking token) must be registered at chain initialization. On Stable this is the STABLE token. 2. The caller must match the validator or delegator whose state is being modified. You cannot delegate on someone else's behalf by calling the precompile directly. ### Unbonding completions When an unbonding period finishes, the tokens become liquid, but the SDK handles this quietly and the EVM doesn't see a direct event. Stable's [system transaction](/en/explanation/system-transactions) mechanism bridges this: the protocol emits an `UnbondingCompleted` event through the `StableSystem` precompile once the unbonding clears, so dApps can subscribe via standard EVM logs. ### When to use it * A staking protocol manages delegation from a vault contract: call `delegate` and `undelegate` as users deposit and withdraw. * A governance dashboard needs a live validator set: use the query methods. * A restaking or liquid-staking product tracks unbonding completions: subscribe to the `UnbondingCompleted` event (see [Tracking unbonding completions](/en/how-to/track-unbonding) once that guide ships). ### Where to find the ABI Full method signatures, struct definitions, and emitted events are in the [Staking precompile reference](/en/reference/staking-module-api). ### Next recommended * [**Staking precompile reference**](/en/reference/staking-module-api) — Call `delegate`, `undelegate`, `redelegate`, and read validator state. * [**System transactions**](/en/explanation/system-transactions) — Learn how unbonding completions reach the EVM as events. * [**Distribution module**](/en/explanation/distribution-module) — Withdraw rewards earned from the delegations managed here. ## System modules Stable's core protocol behavior lives in SDK modules: `x/bank`, `x/distribution`, `x/staking`. To make this behavior accessible from the EVM, Stable exposes each module as a **precompiled contract** at a fixed address. Contracts written in Solidity call the precompile directly, and the EVM routes the call into the native SDK handler. Precompiles are implemented at the protocol level, making them significantly more gas-efficient than an equivalent Solidity re-implementation. ### The three modules | Module | Precompile address | Purpose | | :--------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------- | | [Bank](/en/explanation/bank-module) | `0x0000…1003` (STABLE) | Token transfers, balance accounting, allowance management, mint/burn for authorized contracts. | | [Distribution](/en/explanation/distribution-module) | `0x0000…0801` | Staking-reward claims, reward queries, withdraw-address management. | | [Staking](/en/explanation/staking-module) | `0x0000…0800` | Delegation, undelegation, redelegation, validator queries. | | [System transactions](/en/explanation/system-transactions) | `0x0000…9999` | Protocol-emitted EVM events for SDK-layer operations (e.g. unbonding completions). | Each page above explains what the module does, when to use it, and where to find its ABI. ### Why precompiles, not Solidity Two reasons: * **Gas efficiency.** A precompile runs in the protocol's native execution path. An equivalent Solidity contract would re-implement the same logic with significantly higher gas cost. * **Single source of truth.** Staking, distribution, and token supply are protocol-level state. Exposing them through precompiles avoids maintaining a duplicate Solidity implementation that could drift from the SDK. ### Authorization Some precompile methods (`mint`, `burn`, protocol-level staking operations) require caller authorization. The `x/precompile` module maintains an on-chain whitelist, and calls from unregistered contracts revert. This keeps privileged operations governance-gated without blocking general EVM use of read/transfer methods. ### Next recommended * [**Bank module**](/en/explanation/bank-module) — Understand token transfers, allowances, and the mint/burn authorization model. * [**Staking module**](/en/explanation/staking-module) — See how delegation and validator management reach the EVM. * [**System transactions**](/en/explanation/system-transactions) — Learn how protocol-level events like unbonding completions surface as EVM logs. ## System transactions EVM applications subscribe to on-chain activity through standard interfaces like `eth_getLogs`. But some of the most important operations on Stable (staking unbonding completions, for example) happen inside SDK modules that don't naturally emit EVM events. **System transactions** close this visibility gap: the protocol itself submits EVM transactions that emit events for SDK-layer operations, making them indexable through the same log stream dApps already use. ### Why this matters Consider tracking when a user's tokens finish unbonding. Without system transactions, a dApp would need to either: * Run a separate indexer that watches for SDK events and stores them in its own database. Operational overhead plus a new failure point. * Poll a REST endpoint periodically. 5–10 second latency, higher RPC load, two client stacks (web3 + REST) to maintain. System transactions give dApps real-time event notifications through the same WebSocket connections they already use for EVM logs. No separate indexer. No REST polling. ### How the flow works ``` 1. Protocol event: An SDK-layer operation completes (e.g. staking unbonding). 2. Detection: The x/stable EndBlocker detects the event and queues it in state. 3. System TX: In the next block's PrepareProposal, the protocol generates a system transaction calling the StableSystem precompile. 4. EVM emission: The precompile processes the queued entries and emits standard EVM events — dApps see them through eth_getLogs and subscriptions. ``` The system transaction is created by validators during block proposal, not by users. It lands at the front of the block, before any user transactions. ### The StableSystem precompile Events flow through the `StableSystem` precompile at `0x0000000000000000000000000000000000009999`. Today it emits one event (`UnbondingCompleted`) for staking unbondings. The protocol is designed to extend this to other SDK operations (validator commission changes, governance execution) under the same pattern. ### Security model Two properties keep the event stream trustworthy: * **Protocol-only sender.** System transactions use `0x8888888888888888888888888888888888888888` as their sender. The EVM state-transition rules only allow transactions to the `StableSystem` precompile from this address to skip signature verification. Users cannot forge events or call restricted precompile functions from their own transactions. * **Deterministic emission.** Every honest validator produces the same system transaction for the same protocol events. There's no additional trust assumption beyond standard consensus. ### Batch processing To bound block size, each block processes at most 100 unbonding completions. At Stable's \~700 ms block time, that's roughly 9,000 completions per minute, well above typical staking activity. If a burst exceeds the per-block limit, completions queue in FIFO order and drain over subsequent blocks. There's a one-block (\~700 ms) delay between the SDK event and the EVM emission, which is negligible relative to the 7-day unbonding period itself. ### Where to find the ABI The `StableSystem` interface, event signatures, and sender-authorization rules are in the [System transactions reference](/en/reference/system-transactions-api). ### Next recommended * [**System transactions reference**](/en/reference/system-transactions-api) — Review the `IStableSystem` interface, gas accounting, and authorization rules. * [**Staking module**](/en/explanation/staking-module) — See the SDK operations that surface as system-transaction events. * [**System modules overview**](/en/explanation/system-modules-overview) — Return to the precompile-exposed module list. ## Tech overview :::note **What's live today:** StableBFT consensus, Stable EVM (full EVM compatibility), and the split-path RPC layer are all production-ready. StableDB ships in the v1.4.0 upgrade. Autobahn (DAG-based consensus) and StableVM++ (optimistic parallel execution) are roadmap items. See the [Roadmap](/en/explanation/technical-roadmap) for timelines. ::: You can deploy Solidity or Vyper contracts on Stable today using Hardhat, Foundry, or any standard EVM tooling, and your contracts work without modification. What changes: gas is paid in USDT0, transactions reach single-slot finality, and every layer of the stack is tuned for stablecoin throughput. Tech Overview ### StableBFT **Status: Live** Stable Blockchain leverages **StableBFT**, a customized PoS consensus protocol built on CometBFT, for high throughput, low latency, and strong reliability across the network. :::note **Planned:** DAG-based **Autobahn** consensus with decoupled data dissemination. See the [Roadmap](/en/explanation/technical-roadmap#phase-3-full-stack-optimized-layer-for-usdt). ::: ### Stable EVM **Status: Live** **Stable EVM** is Stable's Ethereum-compatible execution layer. Standard Ethereum tools and wallets interact with the chain unchanged. A set of **precompiles** bridges Stable EVM to the Stable SDK, letting EVM smart contracts call into core chain logic atomically. :::note **Planned:** **StableVM++** with optimistic parallel execution (Block-STM). See the [Roadmap](/en/explanation/technical-roadmap#phase-3-full-stack-optimized-layer-for-usdt). ::: ### StableDB **Status: Ships in v1.4.0** Stable fixes a major blockchain bottleneck: slow disk storage after each block. It separates state commitment from storage so blocks process without delay. `MemDB` and `VersionDB`, powered by `mmap`, keep recent data in memory while older data is stored efficiently, boosting overall throughput. ### High performance RPC **Status: Live** A slow RPC layer ruins the user experience even on a fast chain. Stable addresses this with a **split-path architecture** that separates operations by function, deploying lightweight, specialized RPC nodes for faster response times. :::note **Planned:** RPC nodes optimized for EVM view calls, plus a node-integrated indexer. See the [Roadmap](/en/explanation/technical-roadmap#phase-3-full-stack-optimized-layer-for-usdt). ::: ## Roadmap Stable optimizes every layer of the transaction pipeline (consensus, execution, storage, RPC, and USDT-specific flows) across three phases. This page marks what's shipped, what's in progress, and what's still ahead. Technical Roadmap ### Phase 1: Foundational layer for USDT Status: **Live on mainnet.** #### StableBFT: Live A customized PoS consensus protocol built on CometBFT. Delivers deterministic finality and Byzantine fault tolerance up to one-third of validators. See [Consensus](/en/explanation/consensus) for the current implementation. #### USDT as native gas: Live USDT0 is the native asset for gas payment and value transfer, and simultaneously supports the ERC-20 surface (`approve`, `transfer`, `transferFrom`, `permit`). See [USDT as gas](/en/explanation/usdt-as-gas-token). #### Stable Pay & Stable Name: In progress Stable Pay is a Web2.5 UX wallet experience designed to simplify onboarding for new users while remaining compatible with existing Web3 wallets. Stable Name is a user-friendly aliasing system that replaces raw EVM addresses with human-readable identifiers for sending and receiving tokens. ### Phase 2: Experience layer for USDT Status: **In development.** State DB optimization ships in the v1.4.0 upgrade. The remaining items are still in development. #### State DB optimization: Ships in v1.4.0 Stable decouples state commitment from state storage. Validators commit the latest state in memory while historical state is deferred to disk. `MemDB` and `VersionDB` powered by `mmap` handle real-time commitment without blocking on disk I/O. See [Storage (StableDB)](/en/explanation/stable-db). #### Optimistic parallel execution: Planned Real-world telemetry shows 60–80% of transactions interact with disjoint state and can safely execute in parallel. Stable will execute transactions optimistically under the no-conflict assumption, with rollback and sequential re-execution on detected conflicts. This preserves correctness while improving throughput. #### USDT Transfer Aggregator: Planned Aggregating mechanism that groups USDT0 transfers and processes them collectively, reducing per-transaction overhead and improving overall throughput. See [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator). #### Guaranteed blockspace: Planned Reserved block capacity for enterprise partners, enforced through validator-level reservations and dedicated RPC endpoints. Delivers predictable latency for mission-critical payment flows even under network congestion. See [Guaranteed blockspace](/en/explanation/guaranteed-blockspace). ### Phase 3: Full-stack optimized layer for USDT Status: **Planned.** #### StableBFT on Autobahn DAG-based BFT consensus that integrates naturally with Stable's CometBFT-based consensus layer. See [StableBFT](/en/explanation/consensus) for the current protocol and [Autobahn](/en/explanation/autobahn) for the target architecture. Internal proof-of-concept has demonstrated over 200,000 TPS (consensus only) in controlled environments. #### StableVM++ High-performance execution engine that replaces the Go-based EVM with a C++ implementation. Projected to deliver up to a 6x improvement in EVM execution speed. #### High performance RPC A full RPC stack covering node-level enhancements (real-time chain state processing), node-integrated indexing (low-latency application APIs), scalable pub/sub over WebSocket, and a hybrid load balancer that routes by operation type. See [High performance RPC](/en/explanation/high-performance-rpc) for the current split-path architecture. ### Next recommended * [**Architecture overview**](/en/explanation/core-optimization-overview) — Walk through the current state of the stack the roadmap evolves. * [**Tokenomics**](/en/reference/tokenomics) — Review the economic model that funds validator incentives across the roadmap. * [**Tech overview**](/en/explanation/tech-overview) — Return to the architecture summary. ## Upcoming use cases Stable is building toward payment patterns that go beyond simple transfers and API billing. The cases below cover time-guaranteed settlement, privacy-preserving payments, and autonomous agent commerce. Some are functional today in early form; others depend on Stable features currently in development. ### Guaranteed settlement Reliable payment settlement backed by reserved block capacity, ensuring transaction inclusion regardless of network conditions. #### Concept Some payments are part of a timed settlement cycle, not a standalone transfer. In these flows, settlement must complete before the cycle closes so the next state transition can proceed as scheduled. If the required payments are delayed past that window, the cycle may fail, roll to the next window, or require manual recovery. Stable's [Guaranteed Blockspace](/en/explanation/guaranteed-blockspace) addresses this by reserving execution capacity for qualifying payment flows. The goal is not simply faster settlement, but operationally reliable completion within exact timing constraints. #### Expected scenario A tokenized asset platform runs scheduled DvP (Delivery versus Payment) settlement every few minutes. Cash legs are submitted as a batch, and securities are released only if the full payment batch is included before the current settlement cycle closes. Under normal conditions, this clears immediately. During burst traffic, partial inclusion would create a failed or rolled settlement cycle. With Guaranteed Settlement, the platform reserves capacity for the payment batch so the cycle can close deterministically. #### What enables it Guaranteed blockspace turns on-chain payment into a schedulable operation. Settlement cycles can be designed with hard timing assumptions, batch payments can be committed atomically within a single window, and upstream systems can treat block inclusion as a dependency rather than a hope. ### Confidential payments Privacy-preserving USDT0 transfers where selected transaction details are shielded from public observers while remaining verifiable by the transacting parties and authorized auditors. #### Concept Standard on-chain transfers are fully transparent; anyone can see the sender, recipient, and amount. For business payments, this transparency can expose commercially sensitive information to anyone monitoring the chain. Stable is developing [Confidential Transfer](/en/explanation/confidential-transfer), a privacy layer using zero-knowledge cryptography that enables selective confidentiality for on-chain transactions. The shielded values are accessible only to the involved parties and authorized regulatory auditors. #### Expected scenario A large retailer settles inventory procurement with multiple suppliers on-chain. On a transparent chain, competitors can monitor these transactions to reverse-engineer supplier relationships, order volumes, and wholesale pricing. With confidential transfers, the commercially sensitive details are shielded while the on-chain record still serves as a verifiable settlement receipt for both parties and authorized auditors. ### Agent-to-agent payment Payments initiated and settled between AI agents autonomously, without human approval or intervention in the transaction loop. #### Concept As AI agents take on more operational tasks, they will need to procure services from other agents. In current workflows, this requires a human in the loop to approve each purchase, select a vendor, or verify that the counterparty is trustworthy. Agent-to-agent payment removes that bottleneck by letting agents find, evaluate, and pay for services autonomously within a single transaction loop. This pattern depends on several emerging protocols working together: agent discovery and trust ([ERC-8004](https://eips.ethereum.org/EIPS/eip-8004)), secure communication ([XMTP](https://xmtp.org)), and a payment rail that can settle in real time ([x402](/en/explanation/x402)). #### Expected flow 1. **Discover**: the buyer agent queries an ERC-8004 Identity Registry to find agents that offer the required capability (e.g., image generation). The registry returns matching agent identities with associated metadata. 2. **Verify**: the buyer checks the ERC-8004 registries for each candidate. Identity, reputation scores, and validation proofs determine which providers are trustworthy enough to transact with. 3. **Negotiate**: the buyer sends task parameters to the selected provider over XMTP. The two agents agree on price, deadline, and deliverable format through encrypted messaging. 4. **Pay**: the buyer calls the provider's HTTP endpoint. The provider responds with 402. The buyer signs an ERC-3009 authorization and retries with the payment header. The facilitator settles the payment on Stable, and the provider returns the result. 5. **Rate**: after delivery, the buyer posts feedback to the ERC-8004 Reputation Registry, updating the provider's score for future interactions. #### What enables it Agent-to-agent payment turns service procurement into a fully programmable loop. Agents can compare providers, switch vendors, and settle payments in real time without human scheduling or approval queues. This makes it possible to build autonomous supply chains where agents continuously source, pay for, and deliver services at machine speed, scaling commerce beyond what manual coordination can support. ### Next recommended * [**Guaranteed blockspace**](/en/explanation/guaranteed-blockspace) — Review the protocol-level mechanism behind guaranteed settlement. * [**Confidential transfer**](/en/explanation/confidential-transfer) — See the privacy model Stable is building. * [**x402**](/en/explanation/x402) — Understand the settlement protocol behind agent-to-agent flows. ## USDT as gas **You pay fees in USDT0. No second token, no wrapping, no ETH-equivalent to keep topped up.** USDT0 serves as both the native gas token and an ERC-20 token on the same balance. The same asset that moves as payment also pays for the transaction that moves it. Fees are denominated in dollars, not a volatile native token. This design comes with behavioral differences from Ethereum that affect balance semantics, allowance safety, and certain opcode assumptions. If you're porting a contract from Ethereum, see [USDT0 behavior on Stable](/en/explanation/usdt0-behavior) for the migration checklist before deploying. ### Abstract Stable is an EVM-compatible blockchain that uses USDT0 as its native gas token. USDT0 simultaneously functions as the native asset for gas payment and value transfer, and as an ERC-20 token supporting `approve`, `transfer`, `transferFrom`, and `permit`. This document specifies Stable's USDT0 gas mechanism, describes the resulting behavioral differences, and defines required and recommended development patterns for smart contracts deployed on Stable. ### Version note With Stable v1.2.0, USDT0 becomes the native gas token on Stable, replacing gUSDT. As part of this transition: * gUSDT is being sunset. * Existing gUSDT balances are automatically converted to USDT0. * Users and applications no longer need wrapping and unwrapping flows to pay fees or move value. After v1.2.0, USDT0 serves as both: * the network fee asset (gas), and * a standard ERC20 token with `approve`, `permit`, `transfer`, and `transferFrom`. ### Network addresses USDT0 token contract addresses: * Testnet: [0x78cf24370174180738c5b8e352b6d14c83a6c9a9](https://testnet.stablescan.xyz/token/0x78cf24370174180738c5b8e352b6d14c83a6c9a9) * Mainnet: [0x779ded0c9e1022225f8e0630b35a9b54be713736](https://stablescan.xyz/token/0x779ded0c9e1022225f8e0630b35a9b54be713736) ### Terminology * **Stable**: An EVM-compatible blockchain where USDT0 is the native gas token. * **USDT0**: An omnichain version of USDT that functions both as: * the native asset used for gas and value transfers, and * an ERC20 token with allowance and permit semantics. * **Native balance**: The balance returned by `address(x).balance`, denominated in USDT0. * **Gas fee**: The transaction fee paid in USDT0, calculated under an EIP-1559-style fee market. ### What is USDT0? USDT0 is an omnichain representation of USDT using LayerZero’s Omnichain Fungible Token (OFT) standard. USDT0 is pegged 1:1 with USDT and is designed to move across multiple blockchains without requiring traditional bridge workflows or wrapped representations. When transferring USDT0 across chains, the token is locked on some source chains (depending on the chain’s native USDT support) or burned. It is then minted on the destination chain via LayerZero’s cross-chain messaging. This preserves a 1:1 peg while consolidating liquidity into a single interoperable asset rather than fragmented chain-local pools. For users, this enables faster onboarding, reduced operational complexity, and improved liquidity mobility. ### USDT0 and Stable USDT0 is the core asset that powers Stable’s onchain economics and day-to-day usage. Because the same asset is used for both paying fees and transferring value, Stable reduces friction for: * **Everyday users**: Simpler onboarding and fewer token concepts * **Developers**: Simpler fee and value flows * **Enterprises**: Simplified accounting and treasury operations Stable can also access deep USDT liquidity from day one by enabling users to onboard USDT0 from other networks via LayerZero. ### Assumptions and prerequisites For the content below, you are expected to understand: * Solidity execution semantics and native value transfers * ERC20 allowance mechanics and permit flows * Standard smart contract security patterns, including Checks-Effects-Interactions ### 1. Gas and fee model #### 1.1 Overview Stable denominates all transaction fees in USDT0. Gas pricing follows an EIP-1559-style model with a dynamically adjusting base fee. The transaction fee is defined as: ``` fee = gasUsed × baseFee ``` Transactions may specify `maxFeePerGas` using standard EIP-1559 parameters. *Note: Stable does not support priority tips. Do not set `maxPriorityFeePerGas`, or the tip amount will be lost.* #### 1.2 Transaction submission Clients should fetch the latest base fee from the most recent block and include a safety margin when computing `maxFeePerGas`. Example (illustrative): ```javascript const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const maxPriorityFeePerGas = 1n; const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; ``` #### 1.3 Acquiring USDT0 Accounts obtain USDT0 by: * Bridging USDT0 from other supported chains * Receiving transfers from other accounts on Stable ### 2. How Stable enables USDT0 as the gas token Stable charges gas in USDT0 using a pre-charge and refund settlement model. #### Example transaction Alice sends 100 USDT0 to Bob. #### 2.1 Ante-handler phase During transaction validation in `MonoEVMAnteHandler`: 1. Alice’s USDT0 balance is read. 2. The protocol verifies Alice can cover: * the transaction value (100 USDT0), and * the maximum possible gas fee (`gasWanted × fee`). 3. The maximum gas fee is transferred upfront: * `alice → fee_collector` in USDT0. #### 2.2 Execution phase During `ApplyTransaction`: 1. The EVM executes the transaction. 2. Actual gas consumption is recorded. 3. The value transfer is applied: * `alice → bob` transfers 100 USDT0. #### 2.3 Settlement phase After execution: 1. The protocol computes the unused portion of the pre-charged fee: ``` refund = (gasWanted − gasUsed) × baseFee ``` 2. The unused fee is refunded: * `fee_collector → alice` in USDT0. ### 3. Balance semantics and behavioral differences #### 3.1 Native balance mutability On Ethereum, a contract’s native balance typically changes only as a result of contract execution. On Stable, a contract’s native USDT0 balance may also change due to ERC20 allowance-based operations, including `transferFrom` and `permit`. These operations can reduce a contract’s native balance without invoking any contract code. As a result, the following assumption is invalid on Stable: * A contract’s native balance can only decrease if the contract is called. ### 4. Contract design requirements #### 4.1 Prohibited pattern: mirrored balance accounting Contracts must not rely on internal variables to mirror native balance. Example of an unsafe pattern: ```solidity uint256 public deposited; function deposit() external payable { deposited += msg.value; } ``` Such variables can diverge from the actual native balance if USDT0 is drained through allowance-based transfers. #### 4.2 Required pattern: real-balance solvency checks All native value transfers must verify solvency using `address(this).balance` immediately before transfer. Example: ```solidity require(address(this).balance >= amount, "insufficient balance"); ``` Withdrawals must follow Checks-Effects-Interactions ordering: ```solidity uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount); payable(msg.sender).call{value: amount}(""); ``` #### 4.3 State progression must be balance-independent Protocol logic that depends on progression, milestones, or completion conditions must track these explicitly using non-balance state variables, such as counters or epochs. Native balances must be used only for solvency verification at the moment of payout. #### 4.4 Allowance exposure Contracts that custody user funds should not grant USDT0 allowances to external addresses. If allowances are unavoidable, contracts should: * Approve only exact amounts * Reset allowances immediately after use * Treat residual drain risk as a known limitation ### 5. Address state assumptions #### 5.1 EXTCODEHASH Contracts must not rely on `EXTCODEHASH(addr) == 0x0` to infer that an address has never been used. Any notion of address usage must be tracked explicitly within contract state. Example: ```solidity mapping(address => bool) public used; ``` ### 6. Zero address handling On Stable: * Native USDT0 transfers to `address(0)` revert. * ERC20 USDT0 transfers to `address(0)` also revert. There is no supported mechanism for burning USDT0 by transferring to the zero address. Contracts must: * Explicitly reject `address(0)` as a recipient * Redesign any logic that assumes zero-address burns * Use explicit sink contracts if irreversible loss semantics are required ### 7. Testing requirements Test suites for Stable deployments should include: * Allowance-based drain scenarios (`approve` + `transferFrom`) * Solvency enforcement using real native balance * Address usage logic without reliance on `EXTCODEHASH` * Explicit failure cases for zero-address transfers ### 8. Migration checklist When porting contracts from Ethereum to Stable: * Remove internal native balance mirrors * Replace all solvency checks with `address(this).balance` * Remove all native or ERC20 transfers to `address(0)` * Audit all USDT0 approvals * Add tests covering permit and allowance-based flows ### 9. Summary Stable’s use of USDT0 as a gas token provides predictable fees and unified value accounting while changing core assumptions about native balance behavior. Correct contract design on Stable requires: * Treating USDT0 as a dual-role asset * Enforcing solvency against real balances * Avoiding allowance-based drain paths * Eliminating reliance on Ethereum-specific balance and address assumptions ### FAQ **We’re using USDT0 as the wrapped native token today. After this upgrade, which token should be treated as the wrapped native?** USDT0 becomes both the native token and an ERC-20 token after the upgrade. You should use USDT0 directly, and wrapping or unwrapping is no longer required. **What happens to the original USDT0 contract address (`0x779Ded0c9e1022225f8E0630b35a9b54bE713736`)?** Nothing changes. The same address remains valid and continues to represent USDT0. **After the upgrade, is the native token address `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` (instead of `0x0000000000000000000000000000000000001000`)?** Yes. After the upgrade, the native token identifier/address is `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`. **What about `0x0000000000000000000000000000000000001000`? Is it still used as the token address for gUSDT, and should we keep it on our side?** No. You can remove it. It will not be used after the upgrade. **For DEX calldata, will protocols stop using `0x0000000000000000000000000000000000001000` as the “native token” identifier and use `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` instead?** Correct. After the upgrade, DEXs should use `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` as the native token identifier. ### Next recommended * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Submit a USDT0 transfer on testnet using standard EVM tooling. * [**Gas pricing**](/en/reference/gas-pricing-api) — Build transactions against Stable's single-component fee model. * [**USDT0 behavior on Stable**](/en/explanation/usdt0-behavior) — Audit contracts for dual-role asset semantics, balance reconciliation, and `EXTCODEHASH` behavior. ## Overview Stable's USDT-specific features aren't a menu of independent options. They compose. Each one removes a specific friction that shows up when stablecoin payments move from demos to production. This page explains why the five features exist together. ### The friction stack Most stablecoin payment architecture on general-purpose chains runs into the same stack of problems: 1. **Users have to hold a second token** (ETH, SOL) to pay gas for a transaction that moves stablecoins. An onboarding step that bleeds conversion. 2. **Even with a second token, the user has to cover gas.** This breaks the "send a dollar for a dollar" mental model that merchants and payment apps need. 3. **Transaction costs fluctuate** with network activity. Payroll, treasury ops, and batch settlements can't plan cost or inclusion. 4. **Per-transaction limits cap throughput.** High-volume USDT flows hit chain-wide contention and degrade alongside unrelated activity. 5. **Every transaction is publicly observable.** Supplier payments, salary runs, and treasury moves leak commercially sensitive data. ### How the features compose Stable addresses each friction with a dedicated mechanism: | Friction | Mechanism | Page | | :-------------------------- | :-------------------------------------------- | :------------------------------------------------------------------- | | Separate gas token | USDT0 is the native gas token | [USDT as gas](/en/explanation/usdt-as-gas-token) | | User pays gas at all | Governance-authorized waivers cover gas | [Gas waiver](/en/explanation/gas-waiver) | | Cost and inclusion variance | Reserved block capacity for enrolled partners | [Guaranteed blockspace](/en/explanation/guaranteed-blockspace) | | Throughput ceiling | Parallelized USDT0 transfer batching | [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator) | | Public amount visibility | Selective privacy via ZK cryptography | [Confidential transfer](/en/explanation/confidential-transfer) | Any one of these helps. Taken together, they make Stable a chain where a payments team can model cost, latency, and privacy the same way they would on a traditional card network, with the settlement finality of a blockchain. For the full set of Stable's differences from a general-purpose EVM chain, see [Ethereum comparison](/en/explanation/ethereum-comparison). For a worked example of USDT moving end-to-end through these features, see [Flow of funds](/en/explanation/flow-of-funds). ### Next recommended * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the asset that replaces ETH for gas and payment at once. * [**Flow of funds**](/en/explanation/flow-of-funds) — Trace USDT from on-ramp through on-chain transfer to off-ramp settlement. * [**Bridging to Stable**](/en/explanation/usdt0-bridging) — See how USDT0 moves onto Stable from other chains. ## USDT transfer aggregator The **USDT Transfer Aggregator** bundles USDT0 transfers into parallelized, fault-tolerant batches instead of processing each transfer sequentially. It isolates USDT0 throughput from the rest of the execution pipeline so high-volume stablecoin activity doesn't crowd out other transactions. :::note **Planned.** The aggregator is a forward-looking roadmap item. The content below describes the target design. See [Roadmap](/en/explanation/technical-roadmap) for timing. ::: ### Why it exists Two constraints pull against each other: * Traditional ERC-20 transfers are processed sequentially. Under high load, that's a bottleneck. * Simply giving USDT0 priority would crowd out other transactions and degrade general chain performance. The aggregator resolves this by pulling USDT0 transfers into a dedicated parallel pipeline, leaving the main execution path untouched for everything else. ### Parallel aggregation and verification At the heart of the transfer aggregation system is a parallelizable aggregation and verification pipeline, inspired by the `MapReduce` computational model. Instead of processing each transfer in order, the system performs bundle-level computation, aggregating inputs and outputs across accounts before executing balance updates. #### Key steps 1. **Aggregate Account Diffs** * Each transfer is mapped to a sender and recipient. * A diff journal is generated for each account representing the net token movement: * Negative values for total debits (send). * Positive values for total credits (receive). 2. **Balance Verification** * The system ensures global balance invariants: total input equals total output. * Each account's net change is verified independently in parallel to confirm sufficient funds. * Accounts without sufficient balance are flagged without halting the bundle. 3. **MapReduce Model for Parallelism** * **Map Phase**: Compute the net delta for each account based on all incoming/outgoing transfers. * **Reduce Phase**: Aggregate these deltas to determine the final state update. ### Technical highlights #### Parallel computation model * Leverages parallelism in precompiled contracts to check balances and compute diffs concurrently. * Greatly reduces execution time compared to traditional, sequential ERC20 processing. #### Dependency analysis * Identifies overlapping transfers (e.g., multiple sends from the same account). * Pre-flags high-risk transfers (e.g., likely insufficient funds) to minimize cascading failures. #### Modular failure handling * Transfers are isolated at the account level, so only problematic accounts are affected. * Non-conflicting transfers execute and finalize normally. #### Selective failure handling Traditional transfer handling is all-or-nothing within a block. Stable’s aggregation model introduces granular, per-account failure isolation: * If an account’s `current balance + net diff < 0`, the system marks only that account’s transfers as failed. * Transfers involving other accounts proceed as normal. * This selective rollback mechanism ensures that invalid or malicious transfers do not compromise the integrity of an entire bundle. ### Proposer-driven or reputation-based sorting To further optimize execution and avoid state conflicts, Stable incorporates pre-processing ordering mechanisms for aggregated transfers: * **Reputation-Based Sorting**: Senders with strong histories or proven reliability are prioritized, reducing risk of failures and reordering. * **Proposer-Based Ordering**: Transactions may be sorted by a trusted proposer node that structures the bundle to minimize conflicts and maximize throughput. * **Bundled Transfer Prioritization**: Aggregated USDT transfers are prioritized before general transactions, reducing dependency collisions and unlocking cleaner execution windows. Stable's USDT Transfer Aggregator is a targeted optimization that maximizes throughput for USDT0 transfers without degrading general transaction processing. By combining parallel execution, modular failure handling, and smart ordering strategies, Stable offers a scalable foundation for stablecoin-driven economies. Fast, frequent, and frictionless token transfers are the norm. ### Next recommended * [**Payments use cases**](/en/explanation/payment-use-cases-overview) — See the payment patterns that benefit most from aggregated throughput: P2P, subscriptions, pay-per-call. * [**Execution**](/en/explanation/execution) — See the parallel execution engine the aggregator builds on. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the asset model the aggregator moves. ## USDT0 behavior on Stable **If you're porting a contract from Ethereum, read this page before deploying.** USDT0 on Stable is both the native gas token and an ERC-20 token on the same balance. Four Ethereum-assumed behaviors break as a result: a contract's native balance can change without a call into the contract, `EXTCODEHASH` can oscillate between zero and empty hash, zero-address transfers revert, and a single logical transfer can emit multiple `Transfer` events from fractional-balance reconciliation. This page walks through each case and gives safe contract patterns. If you only read one section, read the [Migration checklist](#migration-checklist). It's the port-your-Ethereum-contract-here summary. ### Dual-role overview USDT0 on Stable is both the native gas token and an ERC-20 token. This dual-role model affects balance behavior, contract design, and event handling. The sections below walk through every case where the dual role changes expected behavior. For background on why USDT0 operates this way, see [USDT as gas](/en/explanation/usdt-as-gas-token). To experience the behavior through real transfers, see [Send your first USDT0](/en/tutorial/send-usdt0). ### Balance reconciliation USDT0 uses 18 decimals as the native asset and 6 decimals as an ERC-20 token. Native transfers and ERC-20 transfers operate on the same underlying balance, but the 12-digit precision gap means the system must reconcile fractional amounts when a transfer involves sub-integer precision. ``` before 0.000001 USDT0 (ERC-20) + 0.000000000000000000 USDT0 (internal) // address(account).balance = 0.000001000000000000 // USDT0.balanceOf(account) = 0.000001 if transfer 0.0000001 USDT0 to another account after 0.000000 USDT0 (ERC-20) + 0.000000900000000000 USDT0 (internal) // address(account).balance = 0.000000900000000000 // USDT0.balanceOf(account) = 0.000000 ``` This can cause `address(account).balance` and `USDT0.balanceOf(account)` to differ by up to 0.000001 USDT0. ### Event handling Each reconciliation transfer emits an additional `Transfer` event. A single logical USDT0 transfer can produce up to two extra `Transfer` events depending on how the sender's and receiver's fractional balances are affected: * **Sender adjustment**: If the sender's fractional balance is insufficient, 0.000001 USDT0 is moved from the sender to the reserve address. This emits an extra `Transfer` event. * **Receiver adjustment**: If the receiver's fractional balance overflows, 0.000001 USDT0 is moved from the reserve address to the receiver. This emits an extra `Transfer` event. * **Both adjustments**: If both conditions occur in the same transfer, the reserve is bypassed. The sender transfers 0.000001 USDT0 directly to the receiver as part of the main transfer. No extra event is emitted. These auxiliary events involve the reserve address `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`. Indexers and off-chain services that track USDT0 balances by replaying `Transfer` events must filter or account for transfers to and from this address. ### Contract design requirements #### Native balance mutability On Ethereum, a contract's native balance typically changes only as a result of contract execution. On Stable, a contract's native USDT0 balance may also change due to ERC-20 allowance-based operations, including `transferFrom` and `permit`. These operations can reduce a contract's native balance without invoking any contract code. As a result, the following assumption is invalid on Stable: > A contract's native balance can only decrease if the contract is called. #### Do not mirror native balance On Ethereum, it is common to track deposits with an internal variable. On Stable, this is unsafe because ERC-20 `transferFrom` can drain the native balance externally. ```solidity // UNSAFE on Stable uint256 public deposited; function deposit() external payable { deposited += msg.value; } ``` #### Always check real balance before transfers All native value transfers must verify solvency using `address(this).balance` just before transfer, not internal accounting variables: ```solidity // SAFE function withdraw() external { uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount, "insufficient balance"); payable(msg.sender).call{value: amount}(""); } ``` #### State progression must be balance-independent Protocol logic that depends on progression, milestones, or completion conditions must track these explicitly using non-balance state variables, such as counters or epochs. Native balances should be used only for solvency verification at the moment of payout. #### No zero-address transfers On Stable, both native and ERC-20 transfers to `address(0)` revert. ```solidity // REVERT on Stable payable(address(0)).call{value: amount}("") USDT0.transfer(address(0), amount); ``` Any contract logic that sends native USDT0 should validate the recipient and explicitly reject `address(0)` before the transfer call: ```solidity // SAFE require(recipient != address(0), "zero address recipient"); payable(recipient).call{value: amount}(""); ``` If a contract uses zero-address transfers as a burn mechanism, it must be redesigned. Use explicit sink contracts if irreversible loss semantics are required. #### EXTCODEHASH behavior On Ethereum, the `EXTCODEHASH` opcode returns: * **Zero hash** (`0x0000...`): if an address has never been used (nonce=0, balance=0, no code). * **Empty hash** (`0xc5d2…a470`, the Keccak-256 hash of empty code): if an address exists but has no code. On Ethereum, once an address transitions from zero hash to empty hash, it cannot return to zero hash. On Stable, because USDT0 supports `permit()`-based approvals, an address can create approvals without sending a transaction. Combined with `transferFrom()`, this allows native balance changes without nonce increment, potentially allowing `EXTCODEHASH` to oscillate between zero hash and empty hash. ```solidity // UNSAFE on Stable function isUnusedAddress(address addr) public view returns (bool) { bytes32 codeHash; assembly { codeHash := extcodehash(addr) } return codeHash == bytes32(0); } ``` Use explicit tracking instead: ```solidity // SAFE contract SafeAddressTracker { mapping(address => bool) public hasBeenUsed; function markAsUsed(address addr) internal { hasBeenUsed[addr] = true; } function isUnused(address addr) public view returns (bool) { return !hasBeenUsed[addr]; } } ``` ### Testing requirements Test suites for Stable deployments should include: * Allowance-based drain scenarios (`approve` + `transferFrom`) * Solvency enforcement using real native balance * Address usage logic without reliance on `EXTCODEHASH` * Explicit failure cases for zero-address transfers ### Migration checklist When porting contracts from Ethereum to Stable: * Remove internal native balance mirrors * Replace all solvency checks with `address(this).balance` * Remove all native or ERC-20 transfers to `address(0)` * Audit all USDT0 approvals * Add tests covering `permit` and allowance-based flows * Verify off-chain indexers handle auxiliary `Transfer` events from fractional balance reconciliation ### Key takeaways Correct contract design on Stable requires: * Treating USDT0 as a dual-role asset * Enforcing solvency against real balances * Avoiding allowance-based drain paths * Eliminating reliance on Ethereum-specific balance and address assumptions Off-chain services and indexers should: * Account for auxiliary `Transfer` events from fractional balance reconciliation * Use direct balance queries instead of event-based balance reconstruction ### Next recommended * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand why USDT0 operates as both the native asset and an ERC-20 token. * [**Send your first USDT0**](/en/tutorial/send-usdt0) — Submit a USDT0 transfer on testnet via native and ERC-20 paths. * [**Ethereum comparison**](/en/explanation/ethereum-comparison) — Review every behavior difference when porting from Ethereum. ## Bridging to Stable USDT reaches Stable via one of two bridge paths, depending on what form it takes on the source chain. Both paths deliver USDT0 into the user's wallet on Stable. :::note **Two paths, one outcome:** * **OFT Mesh**: source chain already has USDT0. Burn on source, mint on Stable. 1:1 across chains. Examples: Arbitrum, Ethereum, Optimism, Polygon, Unichain, Ink, Bera, Mantle, Hyperliquid, MegaETH (21 chains total). * **Legacy Mesh**: source chain has native USDT only. Routes through Arbitrum as hub. 0.03% fee on the transferred amount. Examples: Tron, TON. ::: The sections below describe each path in detail. ### USDT0 OFT Mesh vs Legacy Mesh Stable participates in two complementary cross-chain transfer networks. #### OFT Mesh Any chain that supports USDT0 can participate in the OFT Mesh. Within the OFT Mesh, USDT0 cross-chain transfers maintain a 1:1 value ratio. When a transfer occurs, the USDT0 tokens on the source chain are burned and an equivalent amount is minted on the destination chain. Current OFT Mesh participants include Arbitrum, Bera, Conflux, Ethereum, Flare, Hedera, Hyperliquid, Ink, Mantle, MegaETH, Monad, Morph, MP1, Optimism, Plasma, Polygon, Rootstock, Sei, Stable, Tempo, Unichain, and X Layer. #### Legacy Mesh Any chain with native USDT (rather than USDT0) can route through the Legacy Mesh. The Legacy Mesh follows a hub-and-spoke architecture with Arbitrum serving as the central hub for USDT0. This model leverages a USDT0 liquidity pool on Arbitrum. The USDT0 team charges a 0.03% fee on the transferred amount. Current Legacy Mesh participants include Tron and TON. Ethereum and Arbitrum participate in both meshes: users on these chains can bridge via the OFT path (burn/mint USDT0) or the Legacy path (lock native USDT through the Arbitrum hub). *** ### Path 1: Bridging USDT0 to Stable (OFT-supported chains) This path applies when the user already holds USDT0 on an OFT-supported source chain such as Arbitrum or Ink. #### Actors | Name | On-chain? | Responsible party | | --------------------- | --------- | --------------------------- | | User | N/A | User | | USDT0 OUpgradable | ✅ | Smart contract by USDT0 | | LayerZero Endpoint V2 | ✅ | Smart contract by LayerZero | | MessageLib Registry | ✅ | Smart contract by LayerZero | | Executor | ❌ | LayerZero Labs | | USDT0 DVN | ❌ | USDT0 | | Canary DVN | ❌ | Canary | | LayerZero DVN | ❌ | LayerZero Labs | #### Flow diagram Bridging USDT0 to Stable: OFT Mesh flow #### Detailed steps ##### 1. Initiate transfer (on-chain, source chain) The user calls the `lzSend` method on the **USDT0 OUpgradable** contract on the source chain. The transaction includes the message payload, destination LayerZero endpoint and contract address, and configuration parameters such as gas limits and fees. ##### 2. Packet creation (on-chain, source chain) The source LayerZero Endpoint packages the OApp's message, encodes it using the designated source MessageLib contract, and emits it to the Security Stack (DVNs) and Executor, completing the send transaction. ##### 3. Message verification (off-chain, DVNs) Decentralized Verifier Networks (DVNs) independently verify the message before the destination contract will execute it. Only DVNs authorized by the OApp can perform verification. USDT0 bridging requires three DVNs to sign every message: LayerZero Labs, Canary, and USDT0. For the canonical configuration on any pathway, see [USDT0's OApp on LayerZeroScan](https://layerzeroscan.com/). ##### 4. Mark as verifiable (on-chain, Stable) Once all required DVNs verify the message, the destination MessageLib contract marks it as verifiable. ##### 5. Verification commitment (off-chain, Executor) The Executor commits the verified message to the destination LayerZero Endpoint, preparing it for execution. ##### 6. Packet validation (on-chain, Stable) The destination LayerZero Endpoint confirms that the Executor-delivered packet matches the one verified by the DVNs. ##### 7. Message execution (off-chain, Executor) The Executor invokes `lzReceive` on the destination chain, triggering message processing by the USDT0 OUpgradable contract on Stable. ##### 8. Completion (on-chain, Stable) The USDT0 OUpgradable contract on Stable processes the verified message, completing the cross-chain transfer. USDT0 is minted to the user's address. *** ### Path 2: Bridging native USDT to Stable (Legacy Mesh) This path applies when the user holds native USDT on a Legacy Mesh chain such as Tron. The transfer routes through Arbitrum as an intermediary hub before arriving on Stable. #### Actors | Name | On-chain? | Responsible party | | -------------------------- | --------- | --------------------------- | | User | N/A | User | | USDT Pool | ✅ | Smart contract by USDT0 | | USDT0 Pool | ✅ | Smart contract by USDT0 | | MultiHopComposer | ✅ | Smart contract by LayerZero | | USDT0 OUpgradable | ✅ | Smart contract by USDT0 | | LayerZero Endpoint | ✅ | Smart contract by LayerZero | | MessageLib Registry | ✅ | Smart contract by LayerZero | | USDT0 Legacy Mesh Operator | ❌ | USDT0 | | Executor | ❌ | LayerZero Labs | | USDT0 DVN | ❌ | USDT0 | | Canary DVN | ❌ | Canary | | LayerZero DVN | ❌ | LayerZero Labs | #### Flow diagram Bridging native USDT from Tron to Stable: Legacy Mesh flow #### Detailed steps ##### 1. Initiate transfer (on-chain, Tron) The user initiates the bridge transaction and sends native USDT to the **USDT Pool** contract on Tron. The USDT is locked in the pool. The USDT Pool contract then sends a message to the LayerZero Endpoint contract on Tron. ##### 2. Send message to the Legacy Mesh (off-chain) The LayerZero Endpoint contract emits the message to the **USDT0 Legacy Mesh Operator**, which verifies the message. ##### 3. Initiate MultiHop transfer (on-chain, Arbitrum) The USDT0 Legacy Mesh Operator calls the `lzCompose()` method on the LayerZero **MultiHopComposer** contract on Arbitrum. Without additional user interaction, the MultiHopComposer contract carries out the USDT0 mint-and-burn bridge transfer from Arbitrum to Stable. :::note The MultiHopComposer contract is completely permissionless and has no `owner()` to ensure immutability. ::: ##### 4. Transfer USDT0 to Stable (on-chain and off-chain) The remaining steps follow the exact same path as [bridging USDT0 to Stable](#path-1--bridging-usdt0-to-stable-oft-supported-chains) (steps 1–8 above). The USDT0 OUpgradable contract on Arbitrum sends via LayerZero, DVNs verify, and USDT0 is minted on Stable. #### Things to note * USDT0 liquidity on Arbitrum is managed by the USDT0 team. * The Legacy Mesh incurs a 0.03% fee on the transferred amount. * The user does not need to interact with Arbitrum directly; the MultiHop flow is automatic. ### Next recommended * [**Flow of funds**](/en/explanation/flow-of-funds) — See the end-to-end lifecycle of USDT from on-ramp through settlement. * [**Bridge tutorial**](/en/tutorial/bridge-usdt0) — Bridge Test USDT from Sepolia to Stable testnet using the LayerZero OFT Adapter. * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand what the asset does once it lands on Stable. ## Payments & transfers P2P payments and merchant settlement built around one asset that moves the money and pays for the transaction. ### The problem On general-purpose chains, users must hold a separate gas token (ETH, SOL) just to move stablecoins. That breaks the "send a dollar, receive a dollar" mental model and bleeds conversion at onboarding, where a payer who only has USDT can't even submit the transfer. ### How Stable addresses it * **USDT0 is both the gas token and the payment asset.** A user only ever needs one asset to send or receive. See [USDT as gas](/en/explanation/usdt-as-gas-token). * **Gas waiver lets applications cover gas on behalf of users**, enabling a zero-fee UX without the user touching a second token. See [Gas waiver](/en/explanation/gas-waiver). * **Single-slot finality means settlement is immediate.** Once a transfer is in a block, it's final. See [Ethereum comparison](/en/explanation/ethereum-comparison). ### Next recommended * [**USDT as gas**](/en/explanation/usdt-as-gas-token) — Understand the asset that replaces ETH for gas and payment at once. * [**Gas waiver**](/en/explanation/gas-waiver) — See how applications cover user gas through governance-approved waiver addresses. * [**Ethereum comparison**](/en/explanation/ethereum-comparison) — Review what changes (finality, gas token, priority tips) when moving from Ethereum. ## Payroll & mass payouts Paying employees, contractors, and suppliers at scale, with predictable throughput, predictable cost, and privacy for sensitive amounts. ### The problem High-volume stablecoin disbursements hit per-transaction throughput limits on shared chains. Costs fluctuate with network congestion, so a payroll run that cleared cheaply yesterday can spike today. On top of that, salary and supplier amounts are publicly visible to anyone watching the chain, which leaks commercially sensitive data. ### How Stable addresses it * **The USDT transfer aggregator batches high-volume transfers into parallelized settlement bundles**, so a single run isn't bottlenecked by per-transaction overhead. See [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator). * **Guaranteed blockspace gives enrolled partners reserved capacity in every block**, so inclusion and cost stay predictable regardless of what else the network is doing. See [Guaranteed blockspace](/en/explanation/guaranteed-blockspace). * **Confidential transfer shields amounts using zero-knowledge cryptography**, so payroll and supplier runs don't publish sensitive numbers on-chain. See [Confidential transfer](/en/explanation/confidential-transfer). ### Next recommended * [**USDT transfer aggregator**](/en/explanation/usdt-transfer-aggregator) — Understand how high-volume USDT0 transfers batch into parallelized settlement bundles. * [**Guaranteed blockspace**](/en/explanation/guaranteed-blockspace) — See how enrolled partners secure reserved capacity in every block. * [**Confidential transfer**](/en/explanation/confidential-transfer) — Review how ZK cryptography shields transfer amounts while keeping parties auditable. ## Private transfers Treasury operations, supplier payments, and salary runs where the amount is commercially sensitive and shouldn't be published to the world. ### The problem All standard EVM transfers are publicly observable. A payroll run or supplier settlement leaks business-critical data on-chain: who paid whom, how much, and how often. Competitors, counterparties, and anyone scraping the network can reconstruct payroll bands, vendor pricing, and treasury moves without asking. ### How Stable addresses it * **Confidential transfer uses zero-knowledge cryptography to shield transfer amounts** while keeping the parties auditable for compliance, so sensitive numbers stay private without sacrificing the audit trail. See [Confidential transfer](/en/explanation/confidential-transfer). * **Flow of funds shows where confidential transfer sits** in the full USDT lifecycle from on-ramp to off-ramp. See [Flow of funds](/en/explanation/flow-of-funds). ### Next recommended * [**Confidential transfer**](/en/explanation/confidential-transfer) — Review how ZK cryptography shields transfer amounts while keeping parties auditable. * [**Flow of funds**](/en/explanation/flow-of-funds) — Trace USDT from on-ramp through on-chain transfer to off-ramp settlement. ## Sponsored and gasless experiences Apps that want to remove gas from the user experience entirely, so a first-time user can sign in and transact without acquiring a second asset first. ### The problem Requiring users to acquire a gas token before using an app creates an onboarding cliff that kills conversion for consumer-facing products. A new user who shows up with only USDT (or nothing at all) can't submit a transaction, and pushing them to a separate exchange to buy gas is where most of them drop off. ### How Stable addresses it * **Gas waiver: governance-approved waiver addresses submit wrapper transactions that execute at zero gas price on the user's behalf**, so the app covers gas end-to-end and the user sees a free action. See [Gas waiver](/en/explanation/gas-waiver). * **EIP-7702 session keys let a dApp hold scoped, time-limited permissions**, so it can submit transactions on the user's behalf without the user signing each one. See [EIP-7702](/en/explanation/eip-7702). ### Next recommended * [**Gas waiver**](/en/explanation/gas-waiver) — See how governance-approved waivers submit wrapper transactions at zero gas price. * [**EIP-7702**](/en/explanation/eip-7702) — Understand how EOAs can delegate scoped, time-limited permissions to a dApp. ## Monetize HTTP endpoints x402 is a payment protocol built on HTTP. A server responds with `402 Payment Required` and payment details, a client signs an [ERC-3009](/en/explanation/erc-3009) authorization, and a facilitator settles it on-chain. The entire exchange happens over standard HTTP headers. The client only needs a wallet: no sign-up, no API keys, no card registration. This applies to any scenario where a client pays a server for a resource or service: API access, digital content, merchant checkout, or agent-to-agent payments. ### x402 and MPP x402 is the original HTTP-402 payment protocol. The [Machine Payments Protocol (MPP)](/en/explanation/mpp) is a broader, IETF-track successor that adds payment intents (sessions, subscriptions), multi-rail support (cards, Lightning), and production features (body-digest binding, idempotency). MPP clients are backward compatible: they can call x402 servers without changes. On Stable today, the most direct path is x402 via Semantic Pay or Heurist. To use MPP's wire format on the same USDT0 rail, see [Build an MPP endpoint on Stable](/en/how-to/build-mpp-endpoint). ### What problem does it solve? Paying for a service on the internet today requires user intervention at every step: sign up for an account, sign in, register a payment method. This model does not scale to: * Services too small to justify the infrastructure cost * Transactions too cheap for card network fees * Autonomous agents (AI, bots, IoT devices) that cannot perform sign-up flows With x402, a client only needs a wallet to pay. | **Aspect** | **Traditional billing** | **With x402** | | :-------------------- | :------------------------------ | :--------------------- | | Account required | Yes | No | | API key required | Yes | No | | Minimum viable price | \~$0.30 (card processing floor) | \~$0.001 (on-chain) | | Settlement time | Days (card networks) | Sub-second (on Stable) | | PCI compliance needed | Yes | No | ### How it works #### The three roles **Client** is whoever needs the resource: a web app, a backend service, a CLI tool, or an AI agent. The client only needs a wallet (a private key that can sign ERC-3009 authorizations). **Server** is whoever provides the resource. The server defines what costs how much by attaching x402 middleware to its endpoints. **Facilitator** is the settlement service. It receives the signed payment from the server, verifies it, submits the on-chain transaction, and returns the result. The facilitator never holds the client's funds. The transfer moves directly from client to server within the token contract. On Stable, [Semantic Pay](https://x402.semanticpay.io) operates a public facilitator. #### The payment flow 1. **Client requests a resource.** The client sends a normal HTTP request (GET, POST, etc.) to the server. 2. **Server responds with 402.** The server returns HTTP `402 Payment Required` along with a `PAYMENT-REQUIRED` header containing all the information the client needs: how much to pay, which token, which network, and where to send the funds. 3. **Client signs and resubmits.** The client reads the payment requirements, signs an ERC-3009 authorization for the specified amount, and resubmits the original request with a `PAYMENT-SIGNATURE` header containing the signed authorization. 4. **Facilitator verifies and settles.** The server forwards the signed payment to its facilitator. The facilitator validates the signature, submits the `transferWithAuthorization` call on-chain, and once confirmed, the server returns the requested resource along with a `PAYMENT-RESPONSE` header containing the settlement receipt. #### The three headers All payment information travels through standard HTTP headers, encoded in Base64: | **Header** | **Direction** | **Contents** | | :------------------ | :--------------- | :--------------------------------------------------------------------------- | | `PAYMENT-REQUIRED` | Server to client | Payment scheme, token address, amount, recipient address, network identifier | | `PAYMENT-SIGNATURE` | Client to server | Signed ERC-3009 authorization proving the client has authorized the transfer | | `PAYMENT-RESPONSE` | Server to client | Settlement result including transaction hash and confirmation status | This design works with any HTTP client, any programming language, and any infrastructure that supports custom headers. ### x402 on Stable The x402 protocol defines how payment works over HTTP. Stable provides the settlement environment that makes it practical for production use. #### Sub-second finality Stable's consensus provides sub-second block finality (\~700 milliseconds), allowing x402 facilitators to verify and settle transactions in real time. This is critical for high-frequency automated interactions where AI agents or IoT devices may execute many small payments in rapid succession. #### Single-asset settlement On Stable, USDT0 is both the native gas token and the payment token. The entire x402 payment lifecycle runs on USDT0 alone. The client holds only USDT0, and the facilitator submits transactions using the same token it settles. For AI agents using x402, this means an agent wallet needs to manage only one asset. #### Micro-pricing Prices are denominated in USDT0 atomic units (6 decimals): a cost parameter of `"1000"` translates to exactly $0.001. This precision allows x402 servers to set prices at fractions of a cent. #### Gas waiver integration The [Gas Waiver](/en/how-to/integrate-gas-waiver) eliminates transaction costs entirely. The x402 facilitator can use the Gas Waiver infrastructure to submit `transferWithAuthorization` calls without charging gas to either the buyer or the seller. This means x402 micropayments on Stable carry no overhead beyond the payment amount itself. ### Infrastructure #### Semantic Pay [Semantic Pay](https://x402.semanticpay.io) provides a public x402 facilitator for Stable. It handles signature verification, on-chain submission, and confirmation tracking. Developers integrating x402 on Stable can point their middleware to this endpoint without running their own settlement infrastructure. **Facilitator endpoint:** `https://x402.semanticpay.io` #### WDK (Wallet Development Kit) For AI agents to participate in x402 autonomously, they need wallets they can control programmatically. Tether's open-source WDK provides this: * **Self-custody**: WDK enables AI agents to generate and store private keys locally without relying on centralized API infrastructure. * **x402 compatibility**: The WDK's `WalletAccountEvm` instance natively satisfies the client signer interface required by the x402 SDK, allowing agents to automatically intercept 402 HTTP responses, sign ERC-3009 authorizations, and resubmit requests. **See also:** * [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009): the on-chain settlement standard that x402 uses * [Payment use cases](/en/explanation/payment-use-cases-overview): P2P, subscriptions, invoices, and API billing patterns * [Gas Waiver](/en/how-to/integrate-gas-waiver): zero-cost transaction submission ## 集成 Stable Stable 是一个 Layer 1,其中 USDT0 既是原生 gas 代币,也是一种 ERC-20。单槽最终性、亚秒级出块时间,以及完整的 EVM 兼容性。你只需带上你的钱包、助记词和 USDT0。 选择你正在构建的功能。下面的每条路径都能在几分钟内引导你完成一份可在测试网运行的指南。 ### 选择你的路径 * [**账户**](/cn/explanation/accounts-overview) — 钱包、EIP-7702 委托、会话密钥和支出限额。一流地支持用户账户和代理账户。 * [**支付**](/cn/explanation/payments-overview) — 发送 USDT0,构建 P2P 钱包、周期性订阅、发票结算以及按调用付费的 API。 * [**合约**](/cn/explanation/contracts-overview) — 部署、验证和索引合约。从 Solidity 调用 Bank、Distribution 和 Staking 预编译合约。 * [**AI / 代理**](/cn/explanation/agent-settlement) — 将 MCP 服务器和代理技能接入 AI 编辑器。为自主代理按请求为 API 定价。 * [**基础设施**](/cn/explanation/integrate-overview) — Gas 豁免服务、生态系统提供商(桥、预言机、出入金通道)、网络信息和节点运维。 * [**学习**](/cn/explanation/learn-overview) — 架构、USDT0 行为、用例叙述,以及 Ethereum 到 Stable 的参考资料。 ### 五分钟上手 * [**快速开始**](/cn/tutorial/quick-start) — 连接、从水龙头为钱包注资,并原生发送 0.001 USDT0。 * [**连接到 Stable**](/cn/reference/connect) — 链 ID、RPC 端点、水龙头和区块浏览器。 * [**与 Ethereum 的区别**](/cn/explanation/ethereum-comparison) — 从 Ethereum 移植时哪些保持不变、哪些会改变。 ### 其他内容 * **网络状态与版本**:[测试网](/cn/reference/testnet-information) · [主网](/cn/reference/mainnet-information) · [版本历史](/cn/reference/testnet-version-history)。 * **代币经济学与路线图**:[STABLE 代币经济学](/cn/reference/tokenomics) · [技术路线图](/cn/explanation/technical-roadmap)。 * **常见问题**:[开发者常见问题](/cn/reference/faq) · [开发者支持](/cn/reference/developer-assistance)。 ## 将 USDT0 跨链桥接到 Stable 在本教程中,你将使用 TypeScript 和 ethers v6 以编程方式将 USDT0 从 Ethereum Sepolia 桥接到 Stable Testnet。你将逐步构建这个脚本,每一步添加一个函数。 本教程使用 OFT Mesh 路径。Sepolia 上的 OFT Adapter 锁定你的代币,LayerZero 的双 DVN 验证确认消息,然后在 Stable 上铸造 USDT0。要全面了解其工作原理,请参阅[桥接到 Stable](/cn/explanation/usdt0-bridging)。 :::note 想要更少的代码行数吗?[Stable SDK](/cn/explanation/sdk-overview) 暴露了 `quoteBridge` 和 `bridge`,并会为你选择路由(LayerZero 或 LI.FI)。 ::: ### 前置条件 * Node.js 18.0.0 或更高版本(使用 `node --version` 验证) * 一个你掌控私钥的 Sepolia 钱包(切勿使用持有真实资金的私钥) * 用于支付 gas 的 SepoliaETH(从 [sepoliafaucet.com](https://sepoliafaucet.com) 或 [faucets.chain.link/sepolia](https://faucets.chain.link/sepolia) 获取) * 对在终端中运行脚本有基本的了解 *** ### 1. 设置项目 ```bash mkdir stable-bridge && cd stable-bridge npm init -y npm install ethers@6 @layerzerolabs/lz-v2-utilities npm install -D tsx ``` 你的 `package.json` 应包含: ```json { "name": "stable-bridge", "version": "1.0.0", "scripts": { "bridge": "tsx --env-file=.env bridge.ts" }, "dependencies": { "@layerzerolabs/lz-v2-utilities": "^2.3.39", "ethers": "^6.13.0" }, "devDependencies": { "tsx": "^4.19.0" } } ``` ### 2. 配置你的环境 创建一个包含你的凭据的 `.env` 文件: ```bash PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE SEPOLIA_RPC_URL=https://rpc.sepolia.org ``` 对于 `SEPOLIA_RPC_URL`,以下任何一个都可用: * 公共节点:`https://rpc.sepolia.org` 或 `https://ethereum-sepolia-rpc.publicnode.com` * Alchemy:`https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY` * Infura:`https://sepolia.infura.io/v3/YOUR_KEY` ### 3. 搭建脚本骨架 创建 `bridge.ts`,包含导入、配置和一个 `main` 函数。你将在接下来的步骤中向该文件添加函数,并从 `main` 中调用它们。 ```ts import { ethers, Contract, Wallet, JsonRpcProvider } from "ethers"; import { Options } from "@layerzerolabs/lz-v2-utilities"; const PRIVATE_KEY = process.env.PRIVATE_KEY!; const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org"; // Contract addresses const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927"; const SEPOLIA_OFT_ADAPTER = "0xc099cD946d5efCC35A99D64E808c1430cEf08126"; const STABLE_USDT0 = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; // Destination: Stable Testnet const STABLE_TESTNET_EID = 40374; // Minimal ABIs — only the functions we call const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function approve(address, uint256) returns (bool)", "function allowance(address, address) view returns (uint256)", "function mint(address, uint256)", ]; const OFT_ADAPTER_ABI = [ "function quoteSend((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), bool) view returns ((uint256 nativeFee, uint256 lzTokenFee))", "function send((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), (uint256 nativeFee, uint256 lzTokenFee), address) payable returns ((bytes32, uint64, (uint256, uint256)), (uint256, uint256))", ]; function addressToBytes32(addr: string): string { return ethers.zeroPadValue(ethers.getBytes(ethers.getAddress(addr)), 32); } // You will add functions here. async function main() { const provider = new JsonRpcProvider(SEPOLIA_RPC_URL); const wallet = new Wallet(PRIVATE_KEY, provider); const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet); const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet); const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals) // You will add function calls here. } main().catch((err) => { console.error(err.message); process.exit(1); }); ``` ### 4. 在 Sepolia 上铸造测试 USDT0 Sepolia 上的测试 USDT0 合约暴露了一个公开的 `mint` 函数。将以下函数添加到 `bridge.ts` 中 `main` 的上方: ```ts async function mint(usdt0: Contract, receiver: string, amount: bigint) { console.log(`Minting ${ethers.formatEther(amount)} USDT0 on Sepolia...`); const tx = await usdt0.mint(receiver, amount); await tx.wait(); console.log(`Mint tx: ${tx.hash} confirmed`); const balance = await usdt0.balanceOf(receiver); console.log(`USDT0 balance: ${ethers.formatEther(balance)}`); } ``` 然后从 `main` 中调用它: ```ts await mint(usdt0, wallet.address, amount); ``` 运行脚本: ```bash npx tsx --env-file=.env bridge.ts ``` *** **检查点:** 铸造确认后,你应该会看到日志中记录了一个非零的 USDT0 余额。 *** ### 5. 授权 OFT Adapter 在 OFT Adapter 能够转移你的代币之前,它需要一个 ERC-20 授权额度。将此函数添加到 `main` 上方: ```ts async function approve(usdt0: Contract, spender: string, owner: string, amount: bigint) { console.log("Approving OFT Adapter..."); const tx = await usdt0.approve(spender, amount); await tx.wait(); console.log(`Approve tx: ${tx.hash} confirmed`); const allowance = await usdt0.allowance(owner, spender); console.log(`Allowance: ${ethers.formatEther(allowance)}`); } ``` 在 `main` 中 `mint` 之后添加调用: ```ts // await mint(usdt0, wallet.address, amount); await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); ``` 运行脚本。如果你已经在上一次运行中获得了代币,可以注释掉 `await mint(...)` 调用。 *** **检查点:** 授权确认后,脚本应记录一个非零的授权额度。 *** ### 6. 报价费用并发送桥接交易 `quoteSend` 调用返回以 SepoliaETH 计价的 LayerZero 消息费用,你需要将其作为 `msg.value` 传递给 `send`。将此函数添加到 `main` 上方: ```ts async function send(oftAdapter: Contract, receiver: string, amount: bigint) { const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(); const sendParams = { dstEid: STABLE_TESTNET_EID, to: addressToBytes32(receiver), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: "0x", oftCmd: "0x", }; console.log("Quoting bridge fee..."); const feeResult = await oftAdapter.quoteSend(sendParams, false); const fee = { nativeFee: feeResult.nativeFee, lzTokenFee: feeResult.lzTokenFee }; console.log(`Bridge fee: ${ethers.formatEther(fee.nativeFee)} ETH`); console.log("Sending bridge transaction..."); const tx = await oftAdapter.send(sendParams, fee, receiver, { value: fee.nativeFee, }); await tx.wait(); console.log(`Bridge tx: ${tx.hash} confirmed`); console.log(`Sepolia Etherscan: https://sepolia.etherscan.io/tx/${tx.hash}`); console.log(`LayerZero Scan: https://testnet.layerzeroscan.com/tx/${tx.hash}`); } ``` 在 `main` 中 `approve` 之后添加调用: ```ts // await mint(usdt0, wallet.address, amount); // await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); await send(oftAdapter, wallet.address, amount); ``` ### 7. 验证到达 Stable Testnet 发送后,脚本可以轮询 Stable Testnet RPC,直到代币到达。将此函数添加到 `main` 上方: ```ts async function verify(receiver: string) { console.log("Waiting for DVN verification (~2 minutes)..."); const stableProvider = new JsonRpcProvider("https://rpc.testnet.stable.xyz"); const stableUsdt0 = new Contract(STABLE_USDT0, ["function balanceOf(address) view returns (uint256)"], stableProvider); const before: bigint = await stableUsdt0.balanceOf(receiver); for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 5000)); const current: bigint = await stableUsdt0.balanceOf(receiver); if (current > before) { console.log(`\nUSDT0 on Stable: ${ethers.formatEther(current)}`); console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`); return; } process.stdout.write("."); } console.log("\nTokens have not arrived yet. Check manually:"); console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`); } ``` 在 `main` 中 `send` 之后添加调用: ```ts // await mint(usdt0, wallet.address, amount); // await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); // await send(oftAdapter, wallet.address, amount); await verify(wallet.address); ``` ### 8. 运行完整的桥接流程 你的 `main` 函数现在应该如下所示: ```ts async function main() { const provider = new JsonRpcProvider(SEPOLIA_RPC_URL); const wallet = new Wallet(PRIVATE_KEY, provider); const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet); const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet); const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals) await mint(usdt0, wallet.address, amount); await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount); await send(oftAdapter, wallet.address, amount); await verify(wallet.address); } ``` 运行它: ```bash npx tsx --env-file=.env bridge.ts ``` *** **检查点:** 你应该会看到类似这样的输出: ``` Minting 1.0 USDT0 on Sepolia... Mint tx: 0x3a1f...c9d2 confirmed USDT0 balance: 1.0 Approving OFT Adapter... Approve tx: 0x7b2e...f401 confirmed Allowance: 1.0 Quoting bridge fee... Bridge fee: 0.000101 ETH Sending bridge transaction... Bridge tx: 0xa94f...8c11 confirmed Sepolia Etherscan: https://sepolia.etherscan.io/tx/0xa94f...8c11 LayerZero Scan: https://testnet.layerzeroscan.com/tx/0xa94f...8c11 Waiting for DVN verification (~2 minutes)... ...... USDT0 on Stable: 1.0 ``` 你也可以在 [Stable Testnet 浏览器](https://testnet.stablescan.xyz)上搜索你的钱包地址,以确认铸造事件。 *** ### 你所构建的内容 你已将 USDT0 从 Ethereum Sepolia 桥接到了 Stable Testnet。现在你知道如何: * 使用合约的公开 `mint` 函数在 Sepolia 上铸造测试 USDT0 * 授权 OFT Adapter 代表你花费 ERC-20 代币 * 使用 32 字节地址编码和执行器选项构造 LayerZero `sendParams` * 在提交资金之前使用 `quoteSend` 报价跨链消息费用 * 使用 `send` 执行跨链代币转移,并在目标链上确认送达 * 使用 Stable 的 RPC(`https://rpc.testnet.stable.xyz`,chain ID `2201`)和 Stablescan 验证链上状态 ### 推荐的后续步骤 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 使用桥接得到的 USDT0 进行原生转账和 ERC-20 转账。 * [**桥接到 Stable**](/cn/explanation/usdt0-bridging) — 深入了解 OFT Mesh 与 Legacy Mesh 的机制。 * [**测试网信息**](/cn/reference/testnet-information) — 完整的网络参数、RPC 端点和水龙头详情。 ## 快速开始 你只需要 Node.js、一些来自水龙头的 USDT0 以及一个私钥。Stable 使用 USDT0 作为其 gas 代币,因此你只需要 USDT0 即可进行交易。无需单独为某种 gas 资产注资。 :::note 更喜欢类型化的客户端?[Stable SDK](/cn/explanation/sdk-overview) 封装了 viem,并提供 `transfer`、`bridge` 和 `swap` 方法,让你省去手动处理 ABI 和小数位的工作。 ::: ### 前置条件 * Node.js 20 或更高版本 * 一个你掌控的私钥(一个全新的测试密钥即可) ### 1. 安装与配置 创建一个项目,安装 `ethers`,并保存测试网配置。 ```bash mkdir stable-quickstart && cd stable-quickstart npm init -y && npm install ethers ``` ```text added 1 package, audited 2 packages in 1s ``` 将你的私钥保存到 `.env`: ```bash echo "PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE" > .env ``` 创建 `config.ts`: ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); ``` ### 2. 为钱包注资 打印你的地址,然后从水龙头申请测试网 USDT0。 ```typescript // address.ts import { wallet } from "./config"; console.log("Wallet address:", wallet.address); ``` ```bash npx tsx address.ts ``` ```text Wallet address: 0x1234...abcd ``` 前往 [https://faucet.stable.xyz](https://faucet.stable.xyz),粘贴地址,并选择按钮以接收测试网 USDT0。水龙头会发送 1 USDT0,足以进行数千次原生转账。 ### 3. 发送你的第一笔交易 原生发送 0.001 USDT0。在 Stable 上,USDT0 是原生资产,因此简单的价值转账是最便宜的方式(21,000 gas)。 ```typescript // send.ts import { ethers } from "ethers"; import { provider, wallet } from "./config"; const recipient = "0xRecipientAddress"; // replace with any address const amount = ethers.parseEther("0.001"); // 0.001 USDT0 (18 decimals, native) const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: amount, maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); const receipt = await tx.wait(1); console.log("Tx:", receipt!.hash); console.log("Explorer:", `https://testnet.stablescan.xyz/tx/${receipt!.hash}`); ``` ```bash npx tsx send.ts ``` ```text Tx: 0x8f3a...2d41 Explorer: https://testnet.stablescan.xyz/tx/0x8f3a...2d41 ``` 打开浏览器链接以确认该交易。出块时间大约为 0.7 秒,因此它应该已经最终确认。 :::warning `maxPriorityFeePerGas` 会被 Stable 忽略,并且必须设置为 `0`。请参阅 [Gas 定价](/cn/reference/gas-pricing-api),了解仅基于 base-fee 的模型如何改变交易构造。 ::: ### 接下来去哪里 * [**部署智能合约**](/cn/tutorial/smart-contract) — 搭建一个 Foundry 项目并部署到 Stable 测试网。 * [**构建支付应用**](/cn/how-to/build-p2p-payments) — 创建钱包,发送、接收并查询支付历史。 * [**使用 AI 进行开发**](/cn/how-to/develop-with-ai) — 将 MCP 服务器和 agent 技能接入你的 AI 编辑器。 ## SDK 快速开始 你将安装 `@stablechain/sdk`,创建一个由私钥签名的客户端,在 Stable Testnet 上发送一笔 USDT0 转账,并获取跨链桥和兑换报价。总耗时约五分钟。 :::note Stable 使用 USDT0 作为 gas 代币。你只需要测试网 USDT0 即可进行交易——无需另外充值单独的原生资产。 ::: ### 前提条件 * Node.js 20 或更高版本 * 一个持有测试网 USDT0 的测试私钥。参见 [为你的测试网钱包充值](/cn/how-to/use-faucet)。 ### 1. 安装 ```bash mkdir stable-sdk-quickstart && cd stable-sdk-quickstart npm init -y && npm install @stablechain/sdk viem ``` ```text added 2 packages, audited 3 packages in 2s ``` 保存你的测试密钥: ```bash echo "PRIVATE_KEY=0xYOUR_TEST_KEY" > .env ``` ### 2. 创建客户端 创建 `index.ts`: ```ts import "dotenv/config"; import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const stable = createStable({ network: Network.Testnet, account, }); console.log("Signer:", account.address); ``` ```text Signer: 0xYourAddress ``` `createStable` 接受三种签名模式:`account`(服务端,如上所示)、`transport`(通过 `custom(window.ethereum)` 的浏览器钱包),或 `walletClient`(预先构建的 viem `WalletClient`)。三种方式的完整说明请参见 [将 SDK 与 viem 一起使用](/cn/how-to/sdk-with-viem)。 ### 3. 发送 USDT0 转账 追加到 `index.ts`: ```ts const { txHash } = await stable.transfer({ from: account.address, to: "0x000000000000000000000000000000000000dEaD", amount: 0.001, }); console.log("Transfer:", txHash); ``` 运行它: ```bash npx tsx index.ts ``` ```text Signer: 0xYourAddress Transfer: 0x8f3a...2d41 ``` 在[测试网浏览器](https://testnet.stablescan.xyz)上打开该哈希以确认。 ### 4. 获取跨链桥报价 将 USDT0 从 Ethereum Sepolia 跨链桥接到 Stable Testnet。`quoteBridge` 是一个只读调用——无需签名,无需 gas: ```ts import { Chain } from "@stablechain/sdk"; const bridgeQuote = await stable.quoteBridge({ fromChain: Chain.Sepolia, toChain: Chain.StableTestnet, fromToken: "0xc4DCC311c028e341fd8602D8eB89c5de94625927", toToken: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", amount: 1, }); console.log("Bridge quote:", bridgeQuote); ``` ```text Bridge quote: { toAmount: 0.999812 } ``` 将报价传入 `stable.bridge({ ...params, quote })` 以执行。SDK 为 USDT0 → USDT0 路由选择 LayerZero,其他所有情况则选择 LI.FI。 ### 5. 获取兑换报价 兑换通过 LI.FI 在 Stable 上运行。报价返回预期输出和一个预先构建的交易: ```ts const swapQuote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", amount: 1, fromDecimals: 6, }); console.log("You'll receive:", swapQuote.toAmount, "USDT0"); ``` ```text You'll receive: 0.998 USDT0 ``` 调用 `stable.swap({ ...params, quote: swapQuote })` 以执行。ERC-20 来源的授权由内部处理。 ### 后续推荐 * [**SDK 参考**](/cn/reference/sdk) — 每个参数、返回类型和错误类。 * [**与 viem 一起使用**](/cn/how-to/sdk-with-viem) — 在私钥、浏览器钱包和预先构建的 `WalletClient` 签名之间切换。 * [**与 wagmi 一起使用**](/cn/how-to/sdk-with-wagmi) — 使用 wagmi hooks 将 SDK 接入 React 应用。 ## 发送你的第一笔 USDT0 在 Stable 上,USDT0 既是链的原生资产,也是一个 ERC-20 代币。这意味着 `approve`、`transferFrom` 和 `permit` 在标准价值转账之外仍然完全可用,并且这两条路径都从同一个底层余额中转移资金。 本页将引导你通过这两条路径发送 USDT0,并确认它们都从同一个余额中扣款。 :::note 更喜欢使用带类型的客户端?[Stable SDK](/cn/explanation/sdk-overview) 提供了一个统一的 `transfer({ to, amount, token? })`,它涵盖了两条路径,在链上处理小数位,并为你切换钱包所在的链。 ::: :::note **18 位与 6 位小数**:原生 USDT0 使用 18 位小数(标准 EVM 精度),而 ERC-20 接口报告 6 位小数(标准 USDT 精度)。两者反映的是同一个余额,因此由于小数对账,`address(x).balance` 和 `USDT0.balanceOf(x)` 可能相差最多 0.000001 USDT0。请参阅 [USDT0 在 Stable 上的行为](/cn/explanation/usdt0-behavior)。 ::: ### 你将构建什么 一个两脚本流程:以原生转账方式发送 0.001 USDT0,以 ERC-20 转账方式发送 0.001 USDT0,并打印两个余额。 #### 演示 ```text step 1. Connect wallet → balance displayed 0.01 USDT0 step 2. Send 0.001 USDT0 (choose native or ERC-20 transfer) step 3. Result Sent: 0.001 USDT0 Gas fee: 0.000021 USDT0 Native balance: 0.008979 USDT0 ERC-20 balance: 0.008979 USDT0 ``` ### 前提条件 * Node.js 20 或更高版本 * 一个持有测试网 USDT0 的私钥。请参阅[快速开始](/cn/tutorial/quick-start)为钱包充值。 **USDT0 合约地址** * 主网:`0x779ded0c9e1022225f8e0630b35a9b54be713736` * 测试网:`0x78cf24370174180738c5b8e352b6d14c83a6c9a9` ### 设置 ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); ``` ### 选项 1(推荐):以原生转账方式发送 原生转账的工作方式与在以太坊上发送 ETH 相同。`value` 字段携带 USDT0 数量。一次原生转账仅消耗 21,000 gas,是发送 USDT0 最便宜的方式。 ```typescript // sendNative.ts import { ethers } from "ethers"; import { provider, wallet } from "./config"; const recipient = "0xRecipientAddress"; const amount = ethers.parseUnits("0.001", 18); // 18 decimals for native const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: amount, maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); const receipt = await tx.wait(1); console.log("Native transfer tx:", receipt!.hash); ``` ```bash npx tsx sendNative.ts ``` ```text Native transfer tx: 0x8f3a...2d41 ``` ### 选项 2:以 ERC-20 转账方式发送 USDT0 也可以作为 ERC-20 转账发送。这会从同一个余额中扣款,但使用 6 位小数精度的 ERC-20 接口。 ```typescript // sendERC20.ts import { ethers } from "ethers"; import { wallet, USDT0_ADDRESS } from "./config"; const recipient = "0xRecipientAddress"; const amount = ethers.parseUnits("0.001", 6); // 6 decimals for ERC-20 const usdt0 = new ethers.Contract(USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], wallet); const tx = await usdt0.transfer(recipient, amount); const receipt = await tx.wait(1); console.log("ERC-20 transfer tx:", receipt!.hash); ``` ```bash npx tsx sendERC20.ts ``` ```text ERC-20 transfer tx: 0xa2b1...77c0 ``` ### 验证统一余额 在任一种转账之后,查询两个余额以确认它们来自同一来源。 ```typescript // balances.ts import { ethers } from "ethers"; import { provider, wallet, USDT0_ADDRESS } from "./config"; const nativeBalance = await provider.getBalance(wallet.address); console.log("Native balance:", ethers.formatEther(nativeBalance), "USDT0"); const usdt0 = new ethers.Contract(USDT0_ADDRESS, [ "function balanceOf(address) view returns (uint256)" ], provider); const erc20Balance = await usdt0.balanceOf(wallet.address); console.log("ERC-20 balance:", ethers.formatUnits(erc20Balance, 6), "USDT0"); ``` ```bash npx tsx balances.ts ``` ```text Native balance: 0.008979 USDT0 ERC-20 balance: 0.008979 USDT0 ``` 这两个值代表的是同一个余额。由于[小数余额对账](/cn/explanation/usdt0-behavior#balance-reconciliation),它们可能相差最多 0.000001 USDT0。 ### 推荐的下一步 * [**零 gas 交易**](/cn/how-to/zero-gas-transactions) — 通过豁免服务支付 gas 费来发送 USDT0。 * [**构建 P2P 支付应用**](/cn/how-to/build-p2p-payments) — 创建钱包、发送、接收并查询支付历史。 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 了解双角色余额对账和合约设计。 ## 部署智能合约 在本教程中,你将向 Stable 测试网部署一个简单的智能合约,并从链上读取其状态。在此过程中,你将了解 Stable 网络的配置方式、USDT0 如何作为 gas 代币运作,以及如何将标准 EVM 工具指向 Stable。 本教程假设你对 Solidity 和类 Unix 终端有基本了解。无需任何 Stable 经验。 ### 你将构建什么 一个全新的 Foundry 项目,包含示例 `Counter` 合约,部署到 Stable 测试网,并进行一次状态变更调用和一次读取调用。 #### 演示 ```text step 1. Scaffold Foundry project → stable-hello/ step 2. Configure testnet RPC: https://rpc.testnet.stable.xyz Chain ID: 2201 step 3. Fund wallet from faucet (1 USDT0) step 4. forge create Counter Deployed to: 0xContract... step 5. cast send Counter.setNumber(42) step 6. cast call Counter.number() → 42 ``` ### 前置条件 * 已安装 [Foundry](https://book.getfoundry.sh/getting-started/installation)(`forge`、`cast` 和 `anvil` 已在你的 PATH 中可用) * 一个你掌控私钥的钱包(使用新的测试密钥即可;切勿使用持有真实资金的密钥在测试网上操作) * 可访问测试网 RPC 和水龙头的网络连接 *** ### 1. 创建一个新的 Foundry 项目 运行以下命令搭建一个全新项目: ```bash forge init stable-hello && cd stable-hello ``` Foundry 会创建一个 `src/` 目录,其中包含一个示例 `Counter.sol` 合约和一个匹配的测试文件。你将原样部署此合约。目标是让一些真实的东西上链,而不是编写新颖的 Solidity。 ### 2. 查看你将要部署的合约 打开 `src/Counter.sol`。它包含两个函数: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } ``` `number` 是一个存储在链上的公开状态变量。`increment()` 和 `setNumber()` 是更改它的两种方式。读取 `number` 不需要 gas。它是一次免费的 `eth_call`。 ### 3. 配置 Stable 测试网 在项目根目录创建一个名为 `.env` 的文件来存储你的网络凭据: ```bash touch .env ``` 添加以下内容,并将占位符替换为你的实际私钥: ```bash PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE ``` 接下来,打开 `foundry.toml` 并将 Stable 测试网添加为一个命名的网络配置。在现有的 `[profile.default]` 部分下方追加此代码块: ```toml [rpc_endpoints] stable_testnet = "https://rpc.testnet.stable.xyz" ``` 这告诉 Foundry 当你以 `stable_testnet` 为目标时将交易发送到哪里。Stable 兼容 EVM,因此不需要其他配置。 *** **检查点:** 确认你的 RPC 端点可访问: ```bash cast chain-id --rpc-url https://rpc.testnet.stable.xyz ``` 预期输出: ``` 2201 ``` 链 ID `2201` 是 Stable 测试网。如果你看到这个数字,说明你的机器可以访问该网络。 *** ### 4. 获取你的钱包地址 从你的私钥派生出部署者地址,以便知道要为哪个账户充值: ```bash source .env cast wallet address $PRIVATE_KEY ``` 复制打印出来的地址。你将在下一步中用到它。 ### 5. 用 USDT0 为钱包充值 Stable 使用 **USDT0** 作为其 gas 代币。你用于支付商品和服务的同一种资产被直接用于支付计算费用。没有次要的原生代币。 访问测试网水龙头并请求资金: ``` https://faucet.stable.xyz ``` 粘贴上一步中的地址。水龙头会向你的钱包发送 1 USDT0,足以部署并与多个合约交互。 *** **检查点:** 确认你的余额已到账: ```bash cast balance $PRIVATE_KEY --rpc-url https://rpc.testnet.stable.xyz ``` 你应该会看到一个非零的值。如果余额仍为 `0`,请等待几秒钟并重新运行。Stable 大约每 0.7 秒产生一个新区块,因此资金会很快结算。 *** ### 6. 部署合约 使用 `forge create` 运行部署: ```bash source .env forge create src/Counter.sol:Counter \ --rpc-url https://rpc.testnet.stable.xyz \ --private-key $PRIVATE_KEY \ --broadcast ``` Foundry 会编译合约,广播一笔部署交易,并等待回执。由于出块时间约为 0.7 秒,这只需片刻。 *** **检查点:** 输出应如下所示: ``` [⠒] Compiling... No files changed, compilation skipped Deployer: 0xYourAddress Deployed to: 0xSomeContractAddress Transaction hash: 0xSomeTxHash ``` 复制 `Deployed to` 地址。你将在接下来的两个步骤中用到它。 *** ### 7. 调用写入函数 现在调用 `setNumber()` 在链上存储一个值: ```bash cast send 0xSomeContractAddress "setNumber(uint256)" 42 \ --rpc-url https://rpc.testnet.stable.xyz \ --private-key $PRIVATE_KEY ``` 这会发送一笔交易。你需要为这次状态变更支付一笔少量的 USDT0 费用。值 `42` 现在存储在 Stable 测试网上的 `number` 变量中。 ### 8. 从链上读取状态 调用 `number()` 读回该值。这是一次免费的读取,没有交易也没有 gas: ```bash cast call 0xSomeContractAddress "number()(uint256)" \ --rpc-url https://rpc.testnet.stable.xyz ``` 预期输出: ``` 42 ``` 你刚刚向 Stable 测试网写入并从中读取数据。这次往返——部署、写入、读取——是 EVM 开发的核心循环,它在这里的运作方式与任何其他 EVM 链完全相同。 ### 9. 在 Stablescan 上检查你的部署 打开 Stable 测试网区块浏览器并粘贴你的合约地址: ``` https://testnet.stablescan.xyz ``` 你将看到你的部署交易以及你所做的 `setNumber` 调用。Stablescan 是检查链上状态、验证合约源代码以及读取 Stable 上交易历史的标准工具。 *** ### 你已经构建了什么 你部署了一个合约,发送了一笔状态变更交易,并读取了链上状态——全部在 Stable 测试网上完成。你现在知道如何: * 配置 Foundry(或任何 EVM 工具链)使用标准 RPC 端点以 Stable 为目标 * 使用 USDT0 水龙头为钱包充值 * 使用 USDT0 作为 gas 代币支付交易费用 * 在 Stablescan 上检查你的工作 ### 推荐的后续步骤 * [**验证合约**](/cn/how-to/verify-contract) —— 将你的源代码上传到 Stablescan,以便用户可以阅读并与之交互。 * [**索引合约事件**](/cn/how-to/index-contract) —— 使用 ethers.js 订阅事件并回填历史日志。 * [**Gas 定价参考**](/cn/reference/gas-pricing-api) —— 了解以 USDT0 计价的费用是如何计算的。 ## Brand Kit 您可以在下方找到 Stable 的Brand Kit,其中包含标志的多种格式版本和配色方案。本工具包旨在帮助您在项目或传播中使用 Stable 品牌时保持一致性。 [前往 Stable 品牌资源库](https://www.stable.xyz/brand-kit) ## 结算服务商(Facilitators) 结算服务商(facilitator)负责验证已签名的 x402 支付,并提交在 Stable 上以 USDT0 结算的链上调用。使用托管的结算服务商意味着你无需运行结算基础设施或管理 gas 代币。如需了解通道层面的背景信息,请参阅 [代理结算](/cn/explanation/agent-settlement)。 ### 概览表 | **服务商** | **类别** | **文档 / 快速开始** | **说明** | | :---------------------------------------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------- | | [**Semantic Pay**](https://x402.semanticpay.io) | x402 结算服务商 | [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) | 面向 Stable 的公共 x402 结算服务商;通过 ERC-3009 验证并结算 USDT0 支付 | | [**Heurist**](https://facilitator.heurist.xyz) | x402 结算服务商 | [https://docs.heurist.ai/x402-products/facilitator#supported-networks](https://docs.heurist.ai/x402-products/facilitator#supported-networks) | 支持 Stable 的多链 x402 结算服务商;高吞吐量的验证与结算,并具备 OFAC 筛查 | ### Semantic Pay 面向 AI 代理的支付基础设施:无需信任、点对点、无需许可。Semantic Pay 在 Stable 上作为兼容 x402 的支付结算服务商运行,通过 ERC-3009(`transferWithAuthorization`)结算 USDT0 支付。代理无需在其钱包中持有单独的 gas 代币。 在 Stable 上集成 x402 的开发者只需将其中间件指向 `https://x402.semanticpay.io`,无需任何自定义结算基础设施。 **能力** * 通过 USDT0 进行支付载荷验证(`/verify`)和链上结算(`/settle`) * 使用 ERC-3009 `transferWithAuthorization` 的无 gas 转账 * 用于代理监管的消费限额、审批流程和紧急停止开关 * 从意图到结算的全程可追溯,具备完整的审计日志 * 用于实时支付生命周期更新的事件回调 **结算服务商端点:** `https://x402.semanticpay.io` **文档:** [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) ### Heurist Heurist 运营一个多链 x402 结算服务商,支持 Stable 以及 Base、Base Sepolia 和 X Layer。它面向高频代理工作负载,吞吐量经过调优,每秒可处理数千次支付验证和结算。 将你的中间件指向 `https://facilitator.heurist.xyz`。无需 API 密钥即可开始使用。 **能力** * 从单一端点跨多个网络进行支付验证和链上结算 * 为高频代理流量量身打造的吞吐量 * 自动对发送方地址进行 OFAC 筛查 * 对验证和结算活动的实时可观测性 **结算服务商端点:** `https://facilitator.heurist.xyz` **文档:** [https://docs.heurist.ai/x402-products/facilitator#supported-networks](https://docs.heurist.ai/x402-products/facilitator#supported-networks) ### 如何选择 * 使用托管的结算服务商(Semantic Pay 或 Heurist)可以快速起步。无需运行基础设施,也无需管理 gas 代币。 * 根据你的工作负载中最看重的方面进行选择:如果你想要具备生命周期回调和代理监管控制的 Stable 原生工具,选择 Semantic Pay;如果你需要一个同时覆盖 Stable 及其他 EVM 网络的单一端点,或者 OFAC 筛查是硬性要求,选择 Heurist。 * 如果你需要对结算策略拥有完全控制权、希望将支付数据保留在自己的环境中,或预期的交易量足以证明运维开销是值得的,则可以自行托管。 * 无论你选择哪种方式,都应先用一笔小额支付进行测试,确认验证和结算行为符合你的预期,然后再发送生产流量。 * 这两个结算服务商目前都可在 Stable 上结算 x402。同一个 `/settle` 端点也可以作为 MPP 服务器的链上提交目标,因为 MPP 的线路格式仅在客户端 ↔ 资源服务器这一跳上与 x402 有所不同。参阅 [在 Stable 上构建 MPP 端点](/cn/how-to/build-mpp-endpoint)。 *** 已经有与 Stable 的代理支付集成?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 与我们联系。 ## 钱包 代理钱包为 AI 代理和自主系统提供自托管签名能力,使它们能够参与 x402 支付流程,而无需人工驱动的设置。 ### 概览表 | **提供商** | **类别** | **文档 / 入门** | **备注** | | :---------------------------------------------------------------- | :------- | :------------------------------------------------------------- | :--------------------------------------------------- | | [**Wallet Development Kit (WDK)**](https://docs.wallet.tether.io) | 代理钱包 SDK | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether 的开源 SDK;`WalletAccountEvm` 原生满足 x402 客户端签名器接口 | ### Tether 的 Wallet Development Kit (WDK) 来自 Tether 的开源 SDK,用于构建自托管的 AI 代理钱包。WDK 使代理能够在本地生成和存储私钥,无需依赖基于云的 KMS 或 TEE 基础设施。 WDK 中的 `WalletAccountEvm` 实例原生满足 x402 SDK 所需的客户端签名器接口。配备了 WDK 和 Stable 上的 USDT0 的代理可以自动拦截 402 HTTP 响应、签署 ERC-3009 授权并重新提交请求。 **软件包:** `@tetherto/wdk`、`@tetherto/wdk-wallet-evm` **功能** * 自托管密钥生成和本地存储 * 通过 `WalletAccountEvm` 原生兼容 x402 客户端签名器 * 自动拦截 402 响应并签署 ERC-3009 * 多链支持,包括 Stable **文档:** [https://docs.wallet.tether.io](https://docs.wallet.tether.io) *** 已有与 Stable 集成的代理钱包?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## Bank 预编译合约参考 :::note **概念:** 关于 bank 模块的功能及其使用场景,请参阅 [Bank 模块](/cn/explanation/bank-module)。 ::: ### 摘要 Stable SDK 中的 `x/bank` 模块仅提供基本的代币管理功能。 你可以不受限制地向任何账户转移任何代币,但无法委托其他账户来转移你的代币。 出于这些原因,`bank` 预编译合约在 Stable SDK 现有的 `x/bank` 模块基础上提供了额外的授权和委托功能。 ### 目录 1. **[概念](#concepts)** 2. **[配置](#configuration)** 3. **[方法](#methods)** 4. **[事件](#events)** ### 概念 该预编译合约提供 ERC-20 标准方法——例如用于转账的 `transfer` 和 `balanceOf`,以及用于委托的 `transferFrom`、`approve` 和 `allowance`。你可以直接调用这些方法,无需注册合约地址。 但是,在使用 `mint` 和 `burn` 方法之前,`x/precompile` 模块必须将合约地址加入白名单并进行注册。 ```go func (p *Precompile) mint( ctx sdk.Context, contract *vm.Contract, denom string, method *abi.Method, stateDB vm.StateDB, args []interface{}, ) ([]byte, error) { // ... // mint method is only allowed for the registered caller contract if _, err := precompilecommon.CheckPermissions(ctx, p.precompileKeeper, contract.CallerAddress, CallerPermissions); err != nil { return nil, err } ``` 这一额外的验证流程保证了调用该预编译合约的代币合约是经过授权的。 要在 `x/precompile` 模块白名单中注册代币合约地址及其 denom,你必须提交一个治理提案。 ### 配置 合约地址和 gas 费用是预定义的。 #### 合约地址 * `0x0000000000000000000000000000000000001003` 用于 STABLE(治理代币) ### 方法 #### `mint` 铸造请求数量的新代币并转移到该账户。 要铸造的代币数量必须大于零。 当代币成功铸造并转移到账户时,将发出 `PrecompiledBankMint` 事件。 注意: * 禁止铸造治理代币。 * 调用 mint 方法的调用方合约必须在 x/precompile 模块中注册。 ##### 输入 | 名称 | 类型 | 描述 | | ------ | ------- | --------- | | to | address | 接收铸造代币的地址 | | amount | uint256 | 要铸造的代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------------- | | success | bool | 如果代币成功铸造并转移到账户,则为 true | #### `burn` 从账户中销毁请求数量的代币。 要销毁的代币数量必须大于零。 当代币成功销毁时,将发出 `PrecompiledBankBurn` 事件。 注意: * 禁止销毁治理代币。 * 调用 burn 方法的调用方合约必须在 x/precompile 模块中注册。 ##### 输入 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | from | address | 要销毁代币的地址 | | amount | uint256 | 要销毁的代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果代币成功销毁,则为 true | #### `transfer` 将请求数量的代币从发送方转移给接收方。 代币必须被设置为可发送。要转移的代币数量必须大于零。 当代币成功转移时,将发出 `PrecompiledBankTransfer` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | to | address | 接收代币的地址 | | amount | uint256 | 要转移的代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果代币成功转移,则为 true | #### `transferFrom` 由授权的支出方在授权额度范围内将请求数量的代币从所有者转移给接收方。 代币必须被设置为可发送。 要转移的代币数量必须大于零,且小于或等于当前授权额度。 当代币成功转移时,将发出 `PrecompiledBankTransfer` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | from | address | 转出代币的地址 | | to | address | 接收代币的地址 | | amount | uint256 | 要转移的代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果代币成功转移,则为 true | #### `multiTransfer` 将代币从单个账户转移到多个账户。 代币必须被设置为可发送。 转移给每个接收方的代币数量必须大于零。 当代币成功转移时,将为每个接收方发出 `PrecompiledBankTransfer` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ------ | ---------- | ------------- | | to | address\[] | 接收转移代币的地址 | | amount | uint256\[] | 转移给每个接收方的代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------------- | | success | bool | 如果代币成功转移给每个接收方,则为 true | #### `approve` 授权支出方从所有者的账户转移代币。 要授权的代币数量必须大于零。 当授权成功设置时,将发出 `PrecompiledBankApproval` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ------- | ------- | -------- | | spender | address | 要授权的地址 | | value | uint256 | 要授权的代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果授权成功设置,则为 true | #### `revoke` 撤销支出方从所有者转移代币的授权。 当授权成功撤销时,将发出 `PrecompiledBankRevoke` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ------- | ------- | ------ | | spender | address | 要撤销的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果授权成功撤销,则为 true | #### `balanceOf` 返回账户中的代币余额。 ##### 输入 | 名称 | 类型 | 描述 | | ------- | ------- | --------- | | account | address | 获取代币余额的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ------- | -------- | | balance | uint256 | 账户中的代币数量 | #### `totalSupply` 返回代币的总供应量。 ##### 输入 无 ##### 输出 | 名称 | 类型 | 描述 | | ----------- | ------- | ---- | | totalSupply | uint256 | 代币总量 | #### `allowance` 返回支出方仍被允许从所有者处提取的代币数量。 ##### 输入 | 名称 | 类型 | 描述 | | ------- | ------- | ------ | | owner | address | 所有者的地址 | | spender | address | 支出方的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | amount | uint256 | 已授权的代币数量 | ### 事件 该预编译合约发出的所有事件都以 `PrecompiledBank` 为前缀。 为避免歧义,调用该预编译合约的代币合约应避免使用具有相同前缀的事件名称。 #### PrecompiledBankMint | 名称 | 类型 | 是否索引 | 描述 | | ------ | ------- | ---- | --------- | | from | address | Y | 铸造代币的地址 | | to | address | Y | 接收铸造代币的地址 | | amount | uint256 | N | 铸造的代币数量 | #### PrecompiledBankBurn | 名称 | 类型 | 是否索引 | 描述 | | ------ | ------- | ---- | ------- | | from | address | Y | 销毁代币的地址 | | to | address | Y | 此方法中未使用 | | amount | uint256 | N | 销毁的代币数量 | #### PrecompiledBankTransfer | 名称 | 类型 | 是否索引 | 描述 | | ------ | ------- | ---- | --------- | | from | address | Y | 转移代币的地址 | | to | address | Y | 接收转移代币的地址 | | amount | uint256 | N | 转移的代币数量 | #### PrecompiledBankApproval | 名称 | 类型 | 是否索引 | 描述 | | ------- | ------- | ---- | -------- | | owner | address | Y | 授权代币的地址 | | spender | address | Y | 要授权的地址 | | value | uint256 | N | 已授权的代币数量 | #### PrecompiledBankRevoke | 名称 | 类型 | 是否索引 | 描述 | | ------- | ------- | ---- | -------- | | owner | address | Y | 撤销代币的地址 | | spender | address | Y | 要撤销的地址 | | value | uint256 | N | 已授权的代币数量 | ## 跨链桥 支持在 Stable 之间转移 USDT0 的跨链桥提供商。关于跨链 USDT0 转移的工作原理,请参阅[跨链转入 Stable](/cn/explanation/usdt0-bridging)。如需实操演练,请参阅[将 USDT0 跨链转入 Stable 测试网](/cn/tutorial/bridge-usdt0)教程。 *** ### 支持的源链 任何拥有 USDT0 的链都可以通过 OFT Mesh 跨链转入 Stable。任何拥有原生 USDT 的链都可以通过 Arbitrum 枢纽经由 Legacy Mesh 路由。当前参与者: | 路径 | 示例链 | 机制 | 费用 | | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------ | :------------- | | **OFT Mesh** | Arbitrum、Bera、Conflux、Ethereum、Flare、Hedera、Hyperliquid、Ink、Mantle、MegaETH、Monad、Morph、MP1、Optimism、Plasma、Polygon、Rootstock、Sei、Tempo、Unichain、X Layer | 在源链销毁,在 Stable 铸造 | 仅源链 gas | | **Legacy Mesh** | Tron、TON | 锁定原生 USDT → Arbitrum 枢纽 → 在 Stable 铸造 USDT0 | 0.03% + 源链 gas | Ethereum 和 Arbitrum 同时支持两种路径:持有原生 USDT 的用户可以使用 Legacy Mesh,而持有 USDT0 的用户可以直接使用 OFT Mesh。 *** ### 合约地址 | | 测试网(chain ID 2201) | 主网(chain ID 988) | | :-------------------------- | :------------------------------------------- | :--------------------------------------------------------------------------------------------------- | | **LayerZero EID** | `40374` | 参见 [LayerZero 已部署合约](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable) | | **LayerZero Endpoint V2** | `0x3aCAAf60502791D199a5a5F0B173D78229eBFe32` | 参见 LayerZero 文档 | | **USDT0 token** | `0x78Cf24370174180738C5B8E352B6D14c83a6c9A9` | `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` | | **USDT0 OApp(在 Stable 上)** | N/A | `0xedaba024be4d87974d5aB11C6Dd586963CcCB027` | | **源链 USDT0(Sepolia)** | `0xc4DCC311c028e341fd8602D8eB89c5de94625927` | 在源链上使用主网 USDT0 | | **源链 OApp(Sepolia)** | `0xc099cD946d5efCC35A99D64E808c1430cEf08126` | 在源链上使用主网 OApp | | **LiFi Diamond(Stable 主网)** | N/A | `0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37` | 完整的测试网合约列表(LayerZero endpoint、DVN、executor),请参阅[测试网生态系统合约](/cn/reference/testnet-ecosystem)。 *** ### STABLE OFT 合约 STABLE 代币使用 LayerZero OFT 标准跨链转移到其他链。Stable 上的适配器为出站转移锁定 STABLE;每条远程链上的可升级代理铸造和销毁封装供应量。关于安全模型以及各合约角色的说明,请参阅[跨链桥安全与 DVN](/cn/explanation/bridge-security)。 | 链 | 合约 | 地址 | | :----------- | :---------------------------- | :------------------------------------------- | | **Stable** | `StableOFTAdapter` | `0x386f92606b2D5E0A992ECc3704c31eF39Ff56392` | | **BSC** | `StableOFTUpgradeable`(proxy) | `0x011EBe7d75E2C9D1E0bD0be0bEf5C36f0A90075F` | | **HyperEVM** | `StableOFTUpgradeable`(proxy) | `0xa51dC81944a15623874981181a99D6c56B20ED56` | :::note 即使两者都兼容 EVM,远程链地址也有所不同。在 HyperCore 上激活目标地址需要一笔交易,这会推进部署 nonce,从而产生与 BSC 不同的确定性地址。规范配置请参阅 [stablelabs/chain-oft](https://github.com/stablelabs/chain-oft)。 ::: *** ### Stable 的 DVN 运营商 Stable 的跨链桥运行 3/3 必需 DVN 配置:三个独立运营商必须各自对每条跨链消息签名后才能被接受。没有可选池。三个必需签名者及其 DVN 合约地址: | 运营商 | DVN 地址 | | :----------------- | :------------------------------------------- | | **LayerZero Labs** | `0x9c061c9a4782294eef65ef28cb88233a987f4bdd` | | **Canary** | `0x8d6cc20d84fbeb5733c60436ceb8957da2ac02c8` | | **Horizen** | `0x965a80dc87cec5848310e612dead84b543aef874` | 各路径的链上配置请参阅 [LayerZero 已部署合约](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable)。关于安全原理,请参阅[跨链桥安全与 DVN](/cn/explanation/bridge-security)。 *** ### 跨链桥提供商 | 提供商 | 类型 | 状态 | 描述 | 文档 | | :------------------------------------------------------------------ | :---------- | :---- | :------------------------------- | :--------------------------------------------------------------------------- | | **[LayerZero](https://docs.layerzero.network/v2)** | 跨链消息传递(OFT) | 已上线 | 为 USDT0 OFT 销毁/铸造转移提供支持;双 DVN 验证 | [docs.layerzero.network/v2](https://docs.layerzero.network/v2) | | **[Stargate](https://docs.stargate.finance/introduction/overview)** | 直接桥(流动性池) | 已上线 | 统一流动性池;稳定币优化路由 | [docs.stargate.finance](https://docs.stargate.finance/introduction/overview) | | **[Gas.Zip](https://dev.gas.zip/overview)** | 直接桥(流动性路由) | 已上线 | 跨 350+ 条链的流动性路由;快速终结性 | [dev.gas.zip](https://dev.gas.zip/overview) | | **[LiFi](https://docs.li.fi/api-reference/introduction)** | 跨链桥聚合器 | 已上线 | 跨多个跨链桥和 DEX 兑换路由;SDK + REST API | [docs.li.fi](https://docs.li.fi/api-reference/introduction) | | **[Polymer](https://docs.polymerlabs.org/docs/build/start/)** | 跨链互操作性(IBC) | 集成进行中 | 面向 Ethereum 原生链的基于 IBC 的消息传递 | [docs.polymerlabs.org](https://docs.polymerlabs.org/docs/build/start/) | | **[Relay](https://docs.relay.link/what-is-relay)** | 基于意图的跨链桥 | 集成进行中 | 通过求解器网络实现无 gas 执行 | [docs.relay.link](https://docs.relay.link/what-is-relay) | #### LayerZero 跨链消息传递协议,为 USDT0 OFT 销毁/铸造转移提供支持,采用双 DVN 验证。 **能力** * OFT 标准的源链销毁、目标链铸造转移 * 双 DVN(去中心化验证者网络)消息验证 * 同时为 OFT Mesh 和 Legacy Mesh 路径提供支持 #### Stargate 基于流动性池的跨链桥,针对稳定币路由进行了优化。 **能力** * 跨链统一流动性池 * 稳定币优化路由 * 即时保证终结性 #### Gas.Zip 支持跨 350+ 条链快速转移的流动性路由协议。 **能力** * 跨链流动性路由 * 快速终结性 * 广泛的链覆盖(350+ 条链) #### LiFi 跨多个跨链桥和 DEX 兑换路由转移的跨链桥聚合器。 **能力** * 多桥路由优化 * SDK 和 REST API 集成 * DEX 兑换聚合 #### Polymer 面向 Ethereum 原生链的基于 IBC 的跨链消息传递。集成进行中。 **能力** * 在 Ethereum 上的 IBC 协议消息传递 * 无需外部验证者的原生互操作性 #### Relay 基于意图的跨链桥,通过求解器网络实现无 gas 执行。集成进行中。 **能力** * 基于意图的跨链桥接 * 为用户提供无 gas 执行 * 求解器网络结算 *** ### 费用结构 | 提供商 | 费用模型 | | :------------------------- | :----------------------------------------------------------------------------- | | **LayerZero(OFT Mesh)** | 仅源链 gas(无协议费用) | | **LayerZero(Legacy Mesh)** | 转移金额的 0.03%(由 USDT0 团队收取)+ 源链 gas | | **Stargate** | 适用流动性池费用;参见 [Stargate 文档](https://docs.stargate.finance/introduction/overview) | | **LiFi** | 根据路径可能适用聚合器路由费用 | | **Gas.Zip** | 当前费用表请参见 [Gas.Zip 文档](https://dev.gas.zip/overview) | | **Relay** | 求解器费用;参见 [Relay 文档](https://docs.relay.link/what-is-relay) | *** 有想要集成 Stable 的跨链桥吗?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## 连接 本页整合了连接到 Stable 所需的网络详细信息。 ### 主网 | **字段** | **值** | | :----------- | :----------------------------------------------- | | 网络名称 | Stable Mainnet | | Chain ID | `988` | | 货币符号 | USDT0 | | EVM JSON-RPC | `https://rpc.stable.xyz` | | WebSocket | `wss://rpc.stable.xyz` | | 区块浏览器 | [https://stablescan.xyz](https://stablescan.xyz) | ### 测试网 | **字段** | **值** | | :----------- | :--------------------------------------------------------------- | | 网络名称 | Stable Testnet | | Chain ID | `2201` | | 货币符号 | USDT0 | | EVM JSON-RPC | `https://rpc.testnet.stable.xyz` | | WebSocket | `wss://rpc.testnet.stable.xyz` | | 区块浏览器 | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) | 有关第三方 RPC 提供商,请参阅 [RPC 提供商](/cn/reference/rpc-providers)。如需一个为你预先接入这些端点的类型化客户端,请参阅 [Stable SDK](/cn/explanation/sdk-overview)。 ### 速率限制 公共 RPC 端点(`https://rpc.stable.xyz` 和 `https://rpc.testnet.stable.xyz`)的速率限制为**每个 IP 每 10 秒 1,000 个请求**。超过限制的请求会返回 `HTTP 429`。 如需更高的吞吐量,请使用[第三方 RPC 提供商](/cn/reference/rpc-providers)。 :::note USDT0 作为原生 gas 代币时使用 **18 位小数**(由 `address(x).balance` 返回),作为 ERC-20 代币时使用 **6 位小数**(由 `USDT0.balanceOf(x)` 返回)。这两个接口操作的是同一个底层余额。viem 和 ethers.js 等库报告 18 位小数,因为它们读取的是原生 gas 代币。有关如何协调精度差异的详细信息,请参阅 [USDT0 在 Stable 上的行为](/cn/explanation/usdt0-behavior)。 ::: ### 将 Stable 添加到你的钱包 要手动添加 Stable,请打开浏览器钱包的网络设置,并输入上述表格中的值。必填字段为: * **网络名称** * **RPC URL**(EVM JSON-RPC 端点) * **Chain ID** * **货币符号**:`USDT0` ### 验证连接 通过查询链 ID 确认你的 RPC 端点可访问: ```bash cast chain-id --rpc-url https://rpc.stable.xyz ``` 预期输出: ```text 988 ``` 对于测试网: ```bash cast chain-id --rpc-url https://rpc.testnet.stable.xyz ``` 预期输出: ```text 2201 ``` ### 下一步推荐 * [**快速开始**](/cn/tutorial/quick-start) — 五分钟内发送你的第一笔测试网交易。 * [**获取测试网 USDT0**](/cn/how-to/use-faucet) — 从水龙头为钱包注资或从 Sepolia 跨链转入。 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 在针对余额编写代码之前,了解 18/6 位小数的双重角色。 ## 托管 ### 托管概览表 | **服务商** | **类别** | **文档 / 快速开始** | **备注** | | :---------------------------------------- | :--------- | :----------------------------------------------------------------------------------------------------- | :----------------------- | | [Paxos](https://paxos.com/) | MPC 托管基础设施 | [https://docs.paxos.com/guides/developer/account](https://docs.paxos.com/guides/developer/account) | 受 Mastercard 与 PayPal 信赖 | | [Fireblocks](https://www.fireblocks.com/) | MPC 托管基础设施 | [https://developers.fireblocks.com/docs/quickstart](https://developers.fireblocks.com/docs/quickstart) | 资金管理与结算流程 | | [Fordefi](https://www.fordefi.com/) | MPC 托管基础设施 | [https://docs.fordefi.com/](https://docs.fordefi.com/) | 策略引擎与开发者 API | | [Anchorage](https://www.anchorage.com/) | 受监管的机构级托管 | [https://www.anchorage.com/get-in-touch](https://www.anchorage.com/get-in-touch) | 联邦特许银行;托管规模超 450 亿美元 | ### 类别指南 * **MPC 托管基础设施:** 使用多方计算将私钥控制权分散到多方的平台。它们为机构数字资产运营提供安全的密钥管理、策略引擎和开发者 API。 * **受监管的机构级托管:** 面向需要由特许托管方提供直接监管监督的机构,提供受联邦监管的银行级托管服务。 ### MPC 托管基础设施 #### [Paxos](https://paxos.com/) 一个受监管的区块链与代币化基础设施平台,受到包括 Mastercard 和 PayPal 在内的全球企业的信赖。 **能力** * 受监管的托管与结算基础设施 * 企业级资产保管 * 面向机构的代币化服务 * 稳定币运营的合规框架 **快速开始**:创建开发者账户,并按照 [Paxos 开发者上手指南](https://docs.paxos.com/guides/developer/account) 为 Stable 资产配置托管与结算。 #### [Fireblocks](https://www.fireblocks.com/) 为全球机构提供托管、资金管理与数字资产运营支持的金融基础设施。 **能力** * 基于 MPC 的数字资产托管 * 安全的转账与资金管理流程 * 机构级结算网络 * 稳定币项目基础设施 **快速开始**:按照 [Fireblocks 快速开始指南](https://developers.fireblocks.com/docs/quickstart) 设置工作区,将 Stable 配置为受支持的网络,并开始管理数字资产。 #### [Fordefi](https://www.fordefi.com/) 一个为去中心化金融打造的机构级 MPC 钱包与安全平台。Fordefi 为 Web3 机构提供密钥管理、策略控制和开发者 API。 **能力** * 基于 MPC 的分布式密钥生成与门限签名 * 机构级策略引擎与审批流程 * 用于程序化钱包操作的开发者 API * 浏览器扩展、移动端和 API 接口 **快速开始**:查阅 [Fordefi 开发者文档](https://docs.fordefi.com/) 以创建保险库、配置审批策略,并通过 API 连接到 Stable。 ### 受监管的机构级托管 #### [Anchorage](https://www.anchorage.com/) 一家联邦特许的国家银行,为超过 450 亿美元的数字资产提供安全、受监管的托管。 **能力** * 银行级数字资产托管 * 企业级访问控制 * 受监管的机构运营 * 可审计、合规的资产存储 **快速开始**:通过 Anchorage 的 [机构上手页面](https://www.anchorage.com/get-in-touch) 联系他们,开始为 Stable 资产的受监管托管设置账户。 *** 有托管基础设施希望与 Stable 集成?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## 开发者支持 #### 常见问题(FAQ) * 如何连接 Stable 网络? * 使用标准 JSON-RPC 即可,与常见 EVM 工具兼容。 * 交易费用使用哪种货币? * 所有交易费用以 USDT0 支付。 * Stable 是否支持账户抽象? * 支持。EIP-7702 允许 EOA 临时具备智能账户行为。 * 如何查看交易结果? * 通过余额查询、合约状态或事件日志。 * 如何为 Stable 编写智能合约? * 使用标准 EVM 工具(如 Solidity + JSON-RPC 库)。 完整 FAQ 请见文档。 ### 支持渠道 开发者可直接与 Stable 团队联系以获取技术支持。 * Discord(开发者频道): [https://discord.gg/stablexyz](https://discord.gg/stablexyz) * 问题反馈: 将会在github repo开放后提供 技术支持联系方式将在社区平台上线后持续更新。 ## DEX(去中心化交易所) Stable 上的 DEX 部署,用于现货交易、流动性提供和链上路由。Stable 已列入[官方 Uniswap v3 部署列表](https://gov.uniswap.org/t/official-uniswap-v3-deployments-list/24323/13#p-58106-stable-4):Stable 上的 Uniswap v3 合约经治理认可为规范部署,并通过 [Stable Swap](https://swap.stable.xyz) 作为默认前端进行路由。 ### 概览表 | **提供方** | **类别** | **状态** | **文档 / 入门** | **备注** | | :---------------------------------------- | :-------- | :---------- | :----------------------------------------------------------------- | :------------------------------------------------------------------- | | [**Uniswap v3**](https://swap.stable.xyz) | 集中流动性 AMM | 规范部署(主网已上线) | [docs.uniswap.org](https://docs.uniswap.org/contracts/v3/overview) | 于 2026 年 5 月 12 日被官方 Uniswap v3 部署列表认可。前端:Stable Swap。部署方:Protofire。 | ### Uniswap v3 Stable 上规范的 Uniswap v3 部署,具有集中流动性池和标准费率档位。Stable Swap 是持续维护的默认前端;交易通过下方的合约进行路由。跨链流动性通过 LayerZero 流入。 **功能** * 带有 v3 头寸 NFT 的集中流动性 AMM * 标准 `SwapRouter02`、`Quoter V2` 和 `Universal Router` 集成路径 * 支持 `Permit2` 实现无 gas 授权 * 同时部署了 v2 风格的恒定乘积池以支持旧版路由 #### 主网合约地址 来源:[RFC: Stable Application for Canonical Uniswap v3 Deployment](https://gov.uniswap.org/t/rfc-stable-application-for-canonical-uniswap-v3-deployment/26080) 和[官方 Uniswap v3 部署列表](https://gov.uniswap.org/t/official-uniswap-v3-deployments-list/24323/13#p-58106-stable-4)。 | **合约** | **地址** | | :----------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | | **v3 Core Factory** | [0x88F0a512eF09175D456bc9547f914f48C013E4aA](https://stablescan.xyz/address/0x88F0a512eF09175D456bc9547f914f48C013E4aA) | | **Universal Router** | [0x5Be52b52f3d1dbC324d2959637471a4208626144](https://stablescan.xyz/address/0x5Be52b52f3d1dbC324d2959637471a4208626144) | | **Swap Router02** | [0x32eaf9B5d5F2CD7361c5012890C943D7de84C22a](https://stablescan.xyz/address/0x32eaf9B5d5F2CD7361c5012890C943D7de84C22a) | | **Quoter V2** | [0xb070179E7032CdA868b53e6C1742F80c9e940d1A](https://stablescan.xyz/address/0xb070179E7032CdA868b53e6C1742F80c9e940d1A) | | **Nonfungible Token Position Manager** | [0x3BdC3437405f7D801b6036532713fc1F179136a6](https://stablescan.xyz/address/0x3BdC3437405f7D801b6036532713fc1F179136a6) | | **Nonfungible Token Position Descriptor V1.3.0** | [0x7Cf5987951E48ADf235cc9194bCdc708Eb692D82](https://stablescan.xyz/address/0x7Cf5987951E48ADf235cc9194bCdc708Eb692D82) | | **NFT Descriptor Library V1.3.0** | [0xF7815833076D83161414A46c4E993dC8f22A7ADd](https://stablescan.xyz/address/0xF7815833076D83161414A46c4E993dC8f22A7ADd) | | **Descriptor Proxy** | [0xcd2cD0E139eC5581138E18C6DBB189c53efBAE95](https://stablescan.xyz/address/0xcd2cD0E139eC5581138E18C6DBB189c53efBAE95) | | **Proxy Admin** | [0x51D1E70B8cAbDF4F3aB056475802AB1687b3EA23](https://stablescan.xyz/address/0x51D1E70B8cAbDF4F3aB056475802AB1687b3EA23) | | **Tick Lens** | [0x8dF0D1614aae99352045c62d24d54E72b38111ec](https://stablescan.xyz/address/0x8dF0D1614aae99352045c62d24d54E72b38111ec) | | **v3 Migrator** | [0x2C5f4275F1a278BF328D56CB9db304e915DE3082](https://stablescan.xyz/address/0x2C5f4275F1a278BF328D56CB9db304e915DE3082) | | **v3 Staker** | [0xA32e3E127FF46db40ab3c4775be97ED760AD7178](https://stablescan.xyz/address/0xA32e3E127FF46db40ab3c4775be97ED760AD7178) | | **Permit2** | [0x000000000022D473030F116dDEE9F6B43aC78BA3](https://stablescan.xyz/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | | **Multicall 2** | [0x208099D6E8a107aD485CD1374A6EC5Abd98c7F11](https://stablescan.xyz/address/0x208099D6E8a107aD485CD1374A6EC5Abd98c7F11) | | **V2 Core Factory** | [0x25D2d657F539F2bB16eC82773cBE5ee49ddD3c69](https://stablescan.xyz/address/0x25D2d657F539F2bB16eC82773cBE5ee49ddD3c69) | | **Uniswap V2 Router02** | [0xa571dc7c4f2369F1cA24D3a7E8a35c07Ff52bfC0](https://stablescan.xyz/address/0xa571dc7c4f2369F1cA24D3a7E8a35c07Ff52bfC0) | :::note 截至 2026 年 5 月 12 日,在 2026 年 4 月完成 UAC 治理流程后,Stable 被 Uniswap v3 部署列表所认可。该部署由 Protofire 维护,并通过 LayerZero 实现桥接连接。 ::: #### 获取交易报价 `Quoter V2` 在不执行交易的情况下返回给定输入的预期输出。可从任何指向 Stable RPC 的 EVM 工具中使用它。 ```bash cast call 0xb070179E7032CdA868b53e6C1742F80c9e940d1A \ "quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256)" \ "(,,,,0)" \ --rpc-url https://rpc.stable.xyz ``` ```text (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate) ``` 将 ``、``、`` 和 ``(`100`、`500`、`3000`、`10000` 之一)替换为你要报价的池的值。对于应用集成,建议使用 [Uniswap v3 SDK](https://docs.uniswap.org/sdk/v3/overview) 或指向上述地址的 [Universal Router](https://docs.uniswap.org/contracts/universal-router/overview)。 *** ### 有 DEX 要集成 Stable? 请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系团队,以便列入本页面。 ### 下一步推荐 * [连接到 Stable](/cn/reference/connect):主网和测试网的链 ID、RPC 端点和区块浏览器。 * [桥接](/cn/reference/bridges):将 USDT0 和其他资产转入 Stable 以提供流动性或路由交易。 * [预言机](/cn/reference/oracles):可与交易报价配合使用的价格源,用于定价和清算。 ## Distribution 预编译参考 :::note **概念:** 关于 distribution 模块的功能以及何时使用它,请参阅 [Distribution 模块](/cn/explanation/distribution-module)。 ::: ### 摘要 `distribution` 预编译合约充当桥梁,使 EVM 环境能够使用 Stable SDK 的 `x/distribution` 模块功能。 ### 目录 1. **[概念](#concepts)** 2. **[配置](#configuration)** 3. **[方法](#methods)** 4. **[事件](#events)** ### 概念 `distribution` 预编译合约会执行额外的检查,以确保委托人或存款人是调用者。 ### 配置 合约地址和 gas 成本是预定义的。 #### 合约地址 * `0x0000000000000000000000000000000000000801` ### 方法 #### `setWithdrawAddress` 设置接收委托人委托给验证人的代币奖励的地址。 有时,当委托人是自委托时,验证人地址会被用作委托人。 当提取人地址成功设置时,会发出 `SetWithdrawAddress` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ----------------- | ------- | --------- | | delegatorAddress | address | 委托人的地址 | | withdrawerAddress | address | 接收委托奖励的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ------------------ | | success | bool | 如果提取人地址成功设置则为 true | #### `withdrawDelegatorRewards` 提取委托人将从验证人处获得的奖励。 验证人奖励给委托人的所有类型代币都会在单笔交易中提取。 当奖励成功提取时,会发出 `WithdrawDelegatorRewards` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | delegatorAddress | address | 委托人的地址 | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------ | ------- | ------------- | | amount | Coin\[] | 委托人将收到的各种代币奖励 | `Coin` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ------ | ------- | ----- | | denom | string | 奖励的面额 | | amount | uint256 | 奖励的数量 | #### `withdrawValidatorCommission` 提取验证人的佣金。 验证人作为佣金收到的所有类型代币都会在单笔交易中提取。 当佣金成功提取时,会发出 `WithdrawValidatorCommission` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------ | ------- | ------------- | | amount | Coin\[] | 验证人将收到的各种代币佣金 | #### `validatorDistributionInfo` 返回表示验证人将收到的奖励的分配信息。验证人可以在自己的地址上向自己委托代币以充当委托人,称为自绑定。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ---------------- | ------------------------- | -------- | | distributionInfo | ValidatorDistributionInfo | 验证人的分配信息 | `ValidatorDistributionInfo` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | --------------- | ---------- | --------- | | operatorAddress | address | 验证人操作员的地址 | | selfBondRewards | DecCoin\[] | 验证人的自绑定数量 | | commission | DecCoin\[] | 验证人的佣金 | `DecCoin` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | --------- | ------- | ----- | | denom | string | 奖励的面额 | | amount | uint256 | 奖励的数量 | | precision | uint8 | 奖励的精度 | #### `validatorOutstandingRewards` 返回验证人的未结奖励。未结奖励代表总奖励池:验证人的佣金和自绑定奖励,加上欠所有委托人的总奖励。例如,如果验证人 A 有委托人 B、C 和 D,则未结奖励等于 A 的佣金和自绑定奖励,加上 B、C 和 D 的奖励。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---------- | -------- | | rewards | DecCoin\[] | 验证人的未结奖励 | #### `validatorCommission` 返回验证人的佣金。此方法用于在调用 `withdrawValidatorCommission` 方法之前检索验证人的佣金。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ---------- | ---------- | ------ | | commission | DecCoin\[] | 验证人的佣金 | #### `validatorSlashes` 返回验证人在起始高度和结束高度之间的罚没历史。罚没是指当验证人有恶意行为或违反网络规则(如双重签名、不当行为或不遵守链规则)时所处以的罚款。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证人的地址 | | startingHeight | uint64 | 起始高度 | | endingHeight | uint64 | 结束高度 | | pageRequest | PageReq | 分页请求 | `PageReq` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ---------- | ------ | ------- | | key | bytes | 分页的键 | | offset | uint64 | 分页的偏移量 | | limit | uint64 | 分页的限制 | | countTotal | bool | 是否统计总页数 | | reverse | bool | 是否反向分页 | ##### 输出 | 名称 | 类型 | 描述 | | ---------- | ---------------------- | -------- | | slashes | ValidatorSlashEvent\[] | 验证人的罚没记录 | | pagination | PageResp | 分页响应 | `ValidatorSlashEvent` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | --------------- | ------ | ------ | | validatorPeriod | uint64 | 验证人的周期 | | fraction | Dec | 罚没的比例 | `Dec` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | --------- | ------ | ------- | | value | uint64 | Dec 的值 | | precision | uint8 | Dec 的精度 | `PageResp` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ------- | ------ | ------- | | nextKey | bytes | 分页的下一个键 | | total | uint64 | 总页数 | #### `delegationRewards` 返回委托人从验证人处收到的奖励。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托人的十六进制地址 | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---------- | ------------- | | rewards | DecCoin\[] | 委托人从验证人处收到的奖励 | #### `delegationTotalRewards` 返回委托人从所有验证人处收到的总奖励。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托人的十六进制地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---------------------------- | ---------------- | | rewards | DelegationDelegatorReward\[] | 委托人从所有验证人处收到的总奖励 | | total | DecCoin\[] | 奖励的总数量 | `DelegationDelegatorReward` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ---------------- | ---------- | ------------- | | validatorAddress | address | 验证人的地址 | | reward | DecCoin\[] | 委托人从验证人处收到的奖励 | #### `delegatorValidators` 返回委托人所绑定的验证人。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托人的十六进制地址 | ##### 输出 | 名称 | 类型 | 描述 | | ---------- | --------- | ---------- | | validators | string\[] | 委托人所绑定的验证人 | #### `delegatorWithdrawAddress` 返回由 `setWithdrawAddress` 方法设置的接收委托奖励的地址。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托人的十六进制地址 | ##### 输出 | 名称 | 类型 | 描述 | | --------------- | ------- | --------- | | withdrawAddress | address | 接收委托奖励的地址 | ### 事件 #### SetWithdrawAddress | 名称 | 类型 | 已索引 | 描述 | | --------------- | ------- | --- | ----------- | | caller | address | Y | 调用者(委托人)的地址 | | withdrawAddress | address | N | 接收委托奖励的地址 | #### WithdrawDelegatorRewards | 名称 | 类型 | 已索引 | 描述 | | ---------------- | ------- | --- | ------ | | delegatorAddress | address | Y | 委托人的地址 | | validatorAddress | address | Y | 验证人的地址 | | amount | uint256 | N | 奖励的数量 | #### WithdrawValidatorCommission | 名称 | 类型 | 已索引 | 描述 | | ---------------- | ------- | --- | ------ | | validatorAddress | address | Y | 验证人的地址 | | commission | uint256 | N | 佣金的总数量 | ## EIP-7702 Stable 支持 **EIP-7702**,它允许 EOA 将其账户代码设置为现有的智能合约。EOA 在执行委托方逻辑的同时保留其原始地址和私钥。 :::note **概念:** 关于 EIP-7702 在 Stable 上的作用、委托模型以及安全注意事项,请参阅 [EIP-7702](/cn/explanation/eip-7702)。完整规范请参阅 [EIP-7702 规范](https://eips.ethereum.org/EIPS/eip-7702)。 ::: ### 交易格式 EIP-7702 使用交易类型 `0x04`,并带有 `authorizationList` 字段。每个授权都指定一个委托合约,EOA 在该交易中执行其代码。 ```typescript { type: 4, to: eoa.address, data: delegateCallData, authorizationList: [signedAuthorization], maxPriorityFeePerGas: 0n, // always 0 on Stable // ... standard EIP-1559 fields } ``` 该授权包含: * `chainId`:必须与目标链匹配。 * `address`:委托合约地址。 * `nonce`:授权 nonce(与交易 nonce 分开)。 支持 EIP-7702 的钱包和库会自动处理授权格式。 ### 工具链 * **ethers.js**:`wallet.signAuthorization({ chainId, address, nonce })` 生成已签名的授权,用于包含在 `authorizationList` 中。 * **viem**:使用带有 walletClient 的 `signAuthorization`,然后将结果传递给 `sendTransaction`。 * **Hardhat / Foundry**:当你的工具链版本支持 Pectra 硬分叉时,标准的 EIP-7702 交易格式即可使用。 ### 后续推荐 * [**EIP-7702 概念**](/cn/explanation/eip-7702) — 了解委托模型及何时使用它。 * [**账户抽象(EIP-7702)**](/cn/reference/eip-7702-api) — 逐步实现批量支付、消费限额和会话密钥。 ## 常见问题 ### 常规问题 **什么是 Stable?** Stable 是一个高性能区块链,致力于成为 USDT 的专属区块链,重新定义 USDT 在全球范围内的流通方式。 **Stable 与其他区块链有何不同?** Stable 专为 USDT 打造的高性能网络,具备多种 USDT 专属功能,包括使用 USDT 作为原生 gas、为企业级应用预留专属区块空间和聚合转账功能,所有这些都建立在高度可扩展的架构之上。 ### 技术特性 **Stable 采用什么方法来提升可扩展性?** Stable 采用全栈式优化方案,从状态数据库、执行层、共识机制到 针对 USDT 的特定优化,对区块链上的交易的生命周期的每个阶段都进行了优化。 **Stable 是否可以通过未来的共识升级至 DAG 共识机制?** 可以。与 Narwhal 和 Tusk 不同,它们无法与 StableBFT 直接兼容,而 Autobahn 提供了 PBFT-on-DAG 架构,可以与 Stable 的共识层更自然地集成。 **Stable 是否兼容 EVM?其他 EVM 生态的 dApp 能否迁移到Stable?** Stable 完全兼容 EVM,用户和开发者可以无缝使用现有的以太坊合约、工具和钱包与Stable进行交互。 ### USDT 特性 **如何在 Stable 上获取 USDT0?** 得益于 USDT0 的 OFT 标准,用户可以通过 LayerZero 轻松地将其他网络的 USDT0 跨链转移到 Stable。 **Stable 还有哪些 USDT 专属的独特功能?** 即将上线的功能包括: * **企业专用区块空间**:允许机构用户在网络拥堵时依然获得可预测的延迟和成本。 * **USDT 转账聚合器**:可高效地打包多个 USDT0 转账交易,提高吞吐量并降低成本。 * **保密转账**:在符合监管要求的前提下保护交易金额隐私的功能。 **什么是 Stable Pay?** Stable Pay为用户提供简洁、友好的使用体验,并具备 DeFi 的强大能力。它既适合初学者,也适合有经验的加密用户。新用户可通过社交登录轻松上手,而现有传统区块链钱包的用户也可使用原有方式使用Stable。Stable Pay支持网页与移动端使用,确保用户随时随地安全访问其数字资产。 ## Gas 定价参考 Stable 上的交易构建、gas 估算和工具链配置。 :::note **概念:** 关于 Stable 为何使用单组件费用模型以及它与 Ethereum 的对比,请参阅 [Gas 定价](/cn/explanation/gas-pricing)。 ::: ### 交易构建 在 Stable 上构建交易时,将 `maxPriorityFeePerGas` 设置为 `0`。客户端应从最新区块获取最新的 base fee,并在计算 `maxFeePerGas` 时加入安全余量。 ```javascript // ethers.js v6 const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const maxPriorityFeePerGas = 0n; // always 0 on Stable const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // double the base fee as safety margin const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: parseEther("0.01"), maxFeePerGas, maxPriorityFeePerGas, }); ``` ```text Native USDT0 transfer confirmed. Fee ≈ 0.0000021 USDT0 at baseFee = 1 gwei. ``` ### Gas 估算 像在 Ethereum 上一样使用 `eth_estimateGas` 和 `eth_gasPrice`。关键区别在于 `eth_maxPriorityFeePerGas` 将始终返回 `0`。 ```javascript const gasPrice = await provider.send("eth_gasPrice", []); const gasEstimate = await provider.estimateGas({ to: contractAddress, data: callData, }); const estimatedFeeInUSDT0 = gasPrice * gasEstimate; ``` ### 工具链配置 * **Hardhat / Foundry**:无需特殊配置;标准的 EVM 设置即可工作。如果你的配置显式设置了 priority fee,请将其设置为 `0`。 * **钱包**:隐藏或禁用 priority tip 输入字段。显示它可能会让用户困惑,因为该值不起作用。 * **监控**:费用分析仪表板不应跟踪 priority fee。它们将始终为零。 ### 推荐后续阅读 * [**Gas 定价概念**](/cn/explanation/gas-pricing) — 理解 Stable 为何使用单组件费用模型。 * [**Ethereum 对比**](/cn/explanation/ethereum-comparison) — 了解从 Ethereum 移植时会遇到的所有行为差异。 * [**JSON-RPC API**](/cn/reference/json-rpc-api) — 参考 Stable 提供的 `eth_*` 方法。 ## Gas 豁免协议 本文档规定了 Gas 豁免机制:交易格式、标记路由、治理控制以及 Waiver Server API。 :::note **概念:** 关于 Gas 豁免是什么以及它为何存在,请参阅 [Gas 豁免](/cn/explanation/gas-waiver)。关于针对托管 Waiver Server 的操作集成指南,请参阅 [启用免 Gas 交易](/cn/how-to/integrate-gas-waiver)。 ::: ### 摘要 Gas 豁免通过允许一小组经治理批准的地址(“waivers”)提交 `gasPrice = 0` 的交易,从而在 Stable 上实现面向最终用户的免 Gas 交易。Stable 目前运营一项 waiver 服务(“Waiver Server”),你可以与其集成,以在无需实现特定于协议的封装逻辑的情况下提供免 Gas 的用户体验。 ### 范围 本规范涵盖: * gas 豁免交易的协议级规则 * 封装交易机制和标记地址 * 治理控制的授权与允许的目标 * 用于提交已签名用户交易的 Waiver Server 接口 ### 定义 * **Waiver**:通过验证者治理在链上注册的以太坊地址,被授权提交 gas 豁免交易。 * **InnerTx**:最终用户签名的、`gasPrice = 0` 的交易。 * **WrapperTx**:由 waiver 签名的交易,将用户的 `InnerTx` 传输到链上并授权执行。 * **Marker address(标记地址)**:用于识别 waiver 封装交易的哨兵地址:`0x000000000000000000000000000000000000f333`。 * **AllowedTarget**:将 waiver 限制为特定合约地址和方法选择器的策略。 ### 概述 Gas 豁免使用封装交易模式: 1. 用户签名一笔 `gasPrice = 0` 的 `InnerTx`。 2. waiver 将 `InnerTx` 封装为 `WrapperTx` 并广播。 3. 验证者检测标记交易,验证 waiver 授权和策略约束,然后执行内嵌的 `InnerTx`。 Stable 运营一项 waiver 服务(Waiver Server),它在链上注册为经授权的 waiver。你与 Waiver Server API 集成以提交已签名的 `InnerTx` 载荷。 ### 协议规范 #### 标记地址路由 当且仅当满足以下条件时,交易才被视为 waiver 封装交易: * `to == 0x000000000000000000000000000000000000f333`。 协议将交易的 `data` 字段解释为编码后的内部交易载荷,并使用下面的 waiver 验证规则对其进行处理。 #### 授权与策略检查 对于每个候选封装交易,验证者必须强制执行: 1. **Waiver 授权** * `WrapperTx.from` 必须是通过治理在链上注册的 waiver 地址。 2. **Gas 豁免** * `WrapperTx.gasPrice` 必须等于 `0`。 * `InnerTx.gasPrice` 必须等于 `0`。 3. **目标允许列表** * `InnerTx.to` 以及从 `InnerTx.data` 中提取的方法选择器必须被 waiver 的 `AllowedTarget` 策略允许。 4. **Value 限制** * `WrapperTx.value` 必须等于 `0`。 如果任何检查失败,验证者将拒绝封装交易,且不执行内部交易。 #### 执行语义 如果所有检查均通过: 1. 协议以用户身份执行 `InnerTx`,保留用户的 `from`、`nonce` 和调用语义。 2. Gas 计费由 waiver 机制处理:用户不支付 gas,且根据该功能的定义,waiver 交易使用 `gasPrice = 0`。 3. 封装交易必须提供足够的 `gasLimit` 以覆盖 `InnerTx` 的执行(包括解封装和验证的开销)。 ### 交易格式 #### WrapperTx 封装交易由 waiver 签名并发送到标记地址。 ```javascript WrapperTx { from: waiver_address, to: 0x000000000000000000000000000000000000f333, value: 0, // must be zero data: RLP(InnerTx), // RLP-encoded inner transaction gasPrice: 0, // must be zero gasLimit: sufficient_for_inner, // must cover inner execution + overhead nonce: waiver_nonce } ``` #### InnerTx 内部交易由最终用户签名。 ```javascript InnerTx { from: user_address, to: target_contract, value: value, data: call_data, gasPrice: 0, // must be zero gasLimit: execution_gas, nonce: user_nonce } ``` ### 治理控制的访问 Waiver 授权由验证者治理在链上管理。 治理控制提供: * 可审查的 waiver 地址授权 * waiver 注册和更新的链上透明度 * 撤销能力 * 通过 `AllowedTarget` 进行的按 waiver 范围限定 ### 安全模型 #### 最终用户签名完整性 用户签名 `InnerTx`。waiver 无法在不使签名失效的情况下修改内部交易载荷。你仍必须确保用户仅签名预期的交易载荷。 #### 信任边界 如果合作伙伴通过 Waiver Server 路由提交,则 Gas 豁免会引入一个服务依赖: * 服务的可用性会影响提交免 Gas 交易的能力。 * 授权仍保留在链上;只有已注册的 waiver 地址才能产生有效的封装提交。 ### 集成 你通过以下方式集成: 1. 从用户处收集已签名的 `InnerTx`(`gasPrice = 0`)。 2. 将已签名的内部交易提交到 Waiver Server API。 3. 处理流式返回的结果并向最终用户展示交易哈希。 ### Waiver server #### 概述 Waiver Server 将已签名的用户 `InnerTx` 载荷封装并广播为经 waiver 授权的封装交易。你无需构造封装交易或运营 waiver 地址。 #### 端点和基础 URL 基础 URL: * 主网:TBD * 测试网:`https://waiver.testnet.stable.xyz` #### 认证 除健康检查外,所有端点都需要 bearer token 认证: ``` Authorization: Bearer ``` #### API ##### GET `/v1/health` 健康检查端点。 认证:无。 ##### POST `/v1/submit` 提交一批已签名的内部交易。 认证:必需(`Bearer`)。 请求体: ```json { "transactions": ["0x", "0x"] } ``` 响应以 NDJSON(换行分隔的 JSON)流式返回。每一行对应一个已提交交易的索引。 示例: ```json {"index":0,"id":"abc123","success":true,"txHash":"0x..."} {"index":1,"id":"def456","success":false,"error":{"code":"VALIDATION_FAILED","message":"invalid signature"}} ``` ##### GET `/v1/submit` 用于流式提交的 WebSocket 接口。 认证:必需(`Bearer`)。 #### 集成示例 ```javascript const WAIVER_SERVER = "https://waiver.testnet.stable.xyz"; async function submitGaslessTransaction(signedInnerTxHex, apiKey) { const response = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex], }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); console.log(result); } } } ``` #### 创建用户 InnerTx 你负责构造一个 `gasPrice = 0` 的 `InnerTx`,然后收集用户签名。 示例: ```javascript import { ethers } from "ethers"; async function createInnerTx(userWallet, contractAddress, callData, nonce) { const innerTx = { to: contractAddress, data: callData, value: value, gasPrice: 0, // must be 0 for waiver gasLimit: 100000, nonce: nonce, chainId: 2201, // 988 for mainnet, 2201 for testnet }; return await userWallet.signTransaction(innerTx); } ``` #### 错误码 * `PARSE_ERROR`:解析交易失败 * `INVALID_REQUEST`:请求体格式错误 * `BATCH_SIZE_EXCEEDED`:批量大小超过允许的最大值 * `VALIDATION_FAILED`:交易验证失败 * `BROADCAST_FAILED`:广播到链上失败 * `RATE_LIMITED`:超出速率限制 * `QUEUE_FULL`:服务器队列已满 * `TIMEOUT`:请求超时 ### 推荐的后续内容 * [**零 Gas 交易**](/cn/how-to/zero-gas-transactions) — 以演示为重点的操作演练,附带显示零 Gas 费用的收据。 * [**启用免 Gas 交易**](/cn/how-to/integrate-gas-waiver) — 完整的托管 API 集成指南,包含批量提交和错误处理。 * [**自托管 Gas 豁免**](/cn/how-to/self-hosted-gas-waiver) — 在不使用托管 API 的情况下运行你自己的 waiver 基础设施。 ## 索引器 索引器和分析平台提供对链上数据的结构化访问,使开发者能够大规模查询交易、余额、日志、事件以及应用特定数据。Stable 兼容 EVM,因此标准的以太坊索引工具可无缝运行。 本页列出了当前及即将上线的索引服务提供商,以及开发者可以期待的功能。 ### 概览表 | **提供商** | **类别** | **文档 / 入门** | **备注** | | :------------------------------------------------------------------- | :---------- | :--------------------------------------------------------------------------------------------------------------------------- | :--------------------- | | [**Stablescan**](https://stablescan.xyz/) | 区块链浏览器 | [https://docs.etherscan.io/introduction](https://docs.etherscan.io/introduction) | 区块链浏览器;交易、区块和合约可视化。 | | [**The Graph**](https://thegraph.com/explorer/participants/indexers) | 索引器 | [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) | 使用 GraphQL 构建、部署和查询子图。 | | [**Goldsky**](https://goldsky.com/) | 索引器 | [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) | 高性能索引和实时数据流。 | | [**Ormi Labs**](https://ormilabs.com/) | 索引器 | [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) | 具备实时数据能力的新一代子图索引器。 | | [**Allium**](https://www.allium.so/) | 分析 / 数据平台 | [https://docs.allium.so/](https://docs.allium.so/) | 规范化的区块链数据集与分析工具。 | | [**CoinMarketCap**](https://coinmarketcap.com/api/) | 市场数据聚合器 | [https://coinmarketcap.com/api/documentation/v1/](https://coinmarketcap.com/api/documentation/v1/) | 市场价格、上市信息和追踪工具。 | | [**CoinGecko**](https://www.coingecko.com/en/api) | 市场数据聚合器 | [https://docs.coingecko.com/](https://docs.coingecko.com/) | 独立的市场数据及开发者 API。 | | [**Dexscreener**](https://docs.dexscreener.com/) | DEX 分析 | [https://docs.dexscreener.com/](https://docs.dexscreener.com/) | 实时 DEX 图表、流动性分析和仪表盘。 | | [**DeBank**](https://debank.com/) | 投资组合 / 钱包分析 | [https://cloud.debank.com/](https://cloud.debank.com/) | EVM 钱包追踪、交易和投资组合洞察。 | ### 1. 索引器 索引器将原始区块链数据转换为可搜索、可查询的格式。它们为仪表盘、分析工具、钱包、区块浏览器和应用后端提供支持。 #### The Graph 为超过 75,000 个项目提供数据访问支持的去中心化索引协议。 **功能** * 为 Stable 构建子图 * 基于 GraphQL 的查询 * 分布式索引网络 **文档** 按照此快速入门指南,在几分钟内创建、部署和查询子图: [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) #### Ormi Labs Ormi 是新一代索引器,旨在大规模提供实时和历史区块链数据。 **功能** * 链顶端索引 * 亚秒级查询延迟 * 无代码功能 **文档** 使用 Ormi 开始查询实时数据: [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) 了解如何实时查询 Stable 上的 USDT0 数据: [https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0](https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0) #### Goldsky 具备即时子图和开发者工具的高性能索引平台。 **功能** * 子图部署与管理 * 创建 Webhook 实现实时事件流 * 多子图同步到外部数据库 **文档** [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) 如需全天候支持,请联系 [support@goldsky.com](mailto\:support@goldsky.com)。 ### 2. 分析服务提供商 分析工具帮助团队追踪网络活动、真实世界支付、仪表盘、使用流程和合约交互。 #### Allium 为整个区块链生态系统的工程师和分析师提供的基础数据平台。 **功能** * 规范化的区块链数据集 * 面向分析团队的查询工具 * 企业级数据基础设施 **文档** 开始使用 Allium 的数据平台和 API 资源: [https://www.allium.so/](https://www.allium.so/) **入门**:在 [allium.so](https://www.allium.so/) 注册以获取 API 访问权限,然后使用 REST API 查询规范化的 Stable 链上数据集。 #### CoinMarketCap 全球最大的加密资产市场追踪平台。 **功能** * Stable 资产价格追踪 * 投资组合工具 * 市场数据 API **文档** 探索 CMC API 和集成资源: [https://coinmarketcap.com/api/](https://coinmarketcap.com/api/) **入门**:在 [coinmarketcap.com/api](https://coinmarketcap.com/api/) 注册 API 密钥,并通过 REST API 查询 Stable 资产价格和市场数据。 #### CoinGecko 世界上最大的独立加密数据聚合器。 **功能** * 市场上市信息和价格数据 * 历史分析 * 面向开发者的 API 访问 **文档** 访问 CoinGecko 的 API 文档: [https://www.coingecko.com/en/api](https://www.coingecko.com/en/api) **入门**:从 [coingecko.com/en/api](https://www.coingecko.com/en/api) 获取免费或 Pro API 密钥,并通过 REST API 查询 Stable 代币价格和市场数据。 #### Dexscreener 面向去中心化交易场所的实时图表和分析工具。 **功能** * 实时 DEX 图表 * 流动性和交易对分析 * 交易仪表盘 **文档** 探索 Dexscreener 的 API 端点和开发者工具: [https://docs.dexscreener.com/](https://docs.dexscreener.com/) **入门**:浏览 [Dexscreener API 文档](https://docs.dexscreener.com/),查询基于 Stable 的 DEX 资金池的实时交易对数据、流动性和交易活动。 #### DeBank 面向以太坊和 EVM 生态系统的投资组合追踪器。 **功能** * 钱包分析 * 交易摘要 * 跨链投资组合追踪 **文档** 阅读 DeBank 的 API 参考和集成文档: [https://docs.debank.com/](https://docs.debank.com/) **入门**:注册 [DeBank Cloud](https://cloud.debank.com/) 以获取 API 密钥,并查询 Stable 钱包余额、交易历史和投资组合数据。 *** 有正在集成 Stable 的索引或分析平台吗?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## 结算发票 每张发票都映射到一个唯一的、确定性的 nonce,该 nonce 派生自发票元数据:发票编号、交易方、金额和到期日。此 nonce 通过 [ERC-3009](/cn/explanation/erc-3009) 驱动结算,并创建一个不可变的收据,可与现有会计系统进行对账。 ### 工作原理 买方和供应商各自独立地从相同的发票元数据计算出相同的 nonce。无需外部注册表来协调付款。 nonce 是确定性派生的: ``` nonce = keccak256(invoiceNumber, vendor, buyer, amount, dueDate) ``` 当买方使用此 nonce 签署 ERC-3009 授权时,链上结算事件即可作为防篡改的付款收据。 #### 结算流程 1. **签发发票**:供应商创建一张具有唯一编号、金额和到期日的发票。 2. **计算 nonce**:双方各自独立地从发票元数据派生出相同的 nonce。 3. **买方签名**:买方使用确定性 nonce 在链下签署 ERC-3009 授权。`validBefore` 字段可设置为到期日加上一段宽限期。 4. **结算**:买方或供应商在链上提交 `transferWithAuthorization`。结算在一秒内确认。 5. **对账**:发出的 `AuthorizationUsed` 事件包含该 nonce,将链上结算与确切的发票关联起来。同一交易中的 `Transfer` 事件验证发送方、接收方和金额。 #### 防止重复付款 nonce 在付款时于链上被消耗。同一张发票无法被结算两次;使用已使用过的 nonce 重新提交授权将会被回退。 ### 有何不同 传统的 B2B 发票涉及银行电汇(1–5 个工作日)、人工对账,并且没有与发票本身绑定的加密付款凭证。借助确定性 nonce,链上付款是自我记录的:nonce 将结算与确切的发票关联起来,区块链事件日志提供了不可变的审计追踪。 | **方面** | **传统方式(银行电汇)** | **Stable(ERC-3009)** | | :----- | :----------------- | :------------------------------------ | | 结算 | 1–5 个工作日 | 1 秒以内 | | 对账 | 人工与银行对账单匹配 | `AuthorizationUsed` 事件将付款与发票 nonce 关联 | | 付款凭证 | 银行确认函 | 链上交易,以加密方式与发票关联 | | 中间方 | 代理银行 | 无 | | 手续费 | 电汇费用($15–45)+ 外汇点差 | \~0.00021 USDT0(使用 Gas Waiver 时为 0) | **另请参阅:** * [ERC-3009(带授权转账)](/cn/explanation/erc-3009) * [Gas Waiver](/cn/how-to/integrate-gas-waiver) ## JSON-RPC API ### eth\_namespace | API | support | | ------------------------------------------- | ------- | | eth\_syncing | ✅ | | eth\_gasPrice | ✅ | | eth\_maxPriorityFeePerGas | ✅ | | eth\_feeHistory | ✅ | | eth\_blobBaseFee | ❌ | | eth\_chainId | ✅ | | eth\_blockNumber | ✅ | | eth\_getBalance | ✅ | | eth\_getProof | ✅ | | eth\_getHeaderByNumber | ❌ | | eth\_getHeaderByHash | ❌ | | eth\_getBlockByNumber | ✅ | | eth\_getBlockByHash | ✅ | | eth\_getUncleByBlockNumberAndIndex | ❌ | | eth\_getUncleByBlockHashAndIndex | ❌ | | eth\_getUncleCountByBlockNumber | ❌ | | eth\_getUncleCountByBlockHash | ❌ | | eth\_getCode | ✅ | | eth\_getStorageAt | ✅ | | eth\_getBlockReceipts | ❌ | | eth\_call | ✅ | | eth\_simulateV1 | ❌ | | eth\_estimateGas | ✅ | | eth\_createAccessList | ❌ | | eth\_getBlockTransactionCountByNumber | ✅ | | eth\_getBlockTransactionCountByHash | ✅ | | eth\_getTransactionByBlockNumberAndIndex | ✅ | | eth\_getTransactionByBlockHashAndIndex | ✅ | | eth\_getRawTransactionByBlockNumberAndIndex | ❌ | | eth\_getRawTransactionByBlockHashAndIndex | ❌ | | eth\_getTransactionCount | ✅ | | eth\_getTransactionByHash | ✅ | | eth\_getRawTransactionByHash | ❌ | | eth\_getTransactionReceipt | ✅ | | eth\_sendTransaction | ✅ | | eth\_fillTransaction | ❌ | | eth\_sendRawTransaction | ✅ | | eth\_sign | ✅ | | eth\_signTransaction | ❌ | | eth\_pendingTransactions | ✅ | | eth\_resend | ✅ | | eth\_accounts | ✅ | | eth\_subscribe | ✅ | | eth\_unsubscribe | ✅ | | eth\_getTransactionLogs | ✅ | | eth\_signTypedData | ✅ | | eth\_newPendingTransactionFilter | ✅ | | eth\_newBlockFilter | ✅ | | eth\_newFilter | ✅ | | eth\_getFilterChanges | ✅ | | eth\_getFilterLogs | ✅ | | eth\_uninstallFilter | ✅ | | eth\_getLogs | ✅ | ### debug\_namespace | API | support | | ---------------------------------- | ------- | | debug\_accountRange | ❌ | | debug\_backtraceAt | ❌ | | debug\_blockProfile | ✅ | | debug\_chaindbCompact | ❌ | | debug\_chaindbProperty | ❌ | | debug\_cpuProfile | ✅ | | debug\_dbAncient | ❌ | | debug\_dbAncients | ❌ | | debug\_dbGet | ❌ | | debug\_dumpBlock | ❌ | | debug\_freeOSMemory | ✅ | | debug\_freezeClient | ❌ | | debug\_gcStats | ✅ | | debug\_getAccessibleState | ❌ | | debug\_getBadBlocks | ❌ | | debug\_getRawBlock | ❌ | | debug\_getRawHeader | ❌ | | debug\_getRawTransaction | ❌ | | debug\_getModifiedAccountsByHash | ❌ | | debug\_getModifiedAccountsByNumber | ❌ | | debug\_getRawReceipts | ❌ | | debug\_goTrace | ✅ | | debug\_intermediateRoots | ✅ | | debug\_memStats | ✅ | | debug\_mutexProfile | ✅ | | debug\_preimage | ❌ | | debug\_printBlock | ✅ | | debug\_setBlockProfileRate | ✅ | | debug\_setGCPercent | ✅ | | debug\_setHead | ❌ | | debug\_setMutexProfileFraction | ✅ | | debug\_setTrieFlushInterval | ❌ | | debug\_stacks | ✅ | | debug\_standardTraceBlockToFile | ❌ | | debug\_standardTraceBadBlockToFile | ❌ | | debug\_startCPUProfile | ✅ | | debug\_startGoTrace | ✅ | | debug\_stopCPUProfile | ✅ | | debug\_stopGoTrace | ✅ | | debug\_storageRangeAt | ❌ | | debug\_traceBadBlock | ❌ | | debug\_traceBlock | ❌ | | debug\_traceBlockByNumber | ✅ | | debug\_traceBlockByHash | ✅ | | debug\_traceBlockFromFile | ❌ | | debug\_traceCall | ❌ | | debug\_traceChain | ❌ | | debug\_traceTransaction | ✅ | | debug\_verbosity | ❌ | | debug\_vmodule | ❌ | | debug\_writeBlockProfile | ✅ | | debug\_writeMemProfile | ✅ | | debug\_writeMutexProfile | ✅ | ## 主网信息 访问 Stable 主网所需的全部信息。 ### 网络概览 | 配置项 | 值 | | ------------ | -------------- | | **网络名称** | Stable Mainnet | | **Chain ID** | `988` | | **Gas 代币** | USDT0 | | **治理代币** | STABLE | | **出块时间** | \~0.7 秒 | ### 区块浏览器 | 浏览器 | URL | | -------------- | ------------------------------------------------ | | **Stablescan** | [https://stablescan.xyz](https://stablescan.xyz) | ### RPC 端点 #### 主要端点 | 类型 | 端点 | 用途 | | ---------------- | ------------------------------------------------ | ------ | | **EVM JSON-RPC** | [https://rpc.stable.xyz](https://rpc.stable.xyz) | EVM 交易 | | **WebSocket** | wss\://rpc.stable.xyz | 实时更新 | :::note 公共 RPC 端点的速率限制为 **每个 IP 每 10 秒 1,000 次请求**。超过限制的请求将返回 `HTTP 429`。如需更高的吞吐量,请使用[第三方 RPC 提供商](/cn/reference/rpc-providers)。 ::: ### 链信息 | 参数 | EVM | | ------------ | ------- | | **Chain ID** | `988` | | **Gas 代币** | `USDT0` | | **小数位** | 18 | ### 工具 | 工具 | URL | 说明 | | ------ | ------------------------------------------ | --- | | **快照** | 参见[节点运营者指南](/cn/how-to/use-node-snapshots) | 链快照 | ## 版本历史 Stable 主网的完整版本历史及相关文档。 ### 当前版本信息 * **当前版本**: `v1.3.1` * **下次升级**: `TBD` * **升级高度**: `TBD` * **预计时间**: `TBD` ### 版本历史 #### 当前与历史版本 | 版本 | Commit | 升级高度 | 二进制文件 | 状态 | | ---------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | **v1.3.1** | `f85d155` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.1-linux-arm64-mainnet.tar.gz) | 当前 | | **v1.3.0** | `dd103ec` | 24,077,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.3.0-linux-arm64-mainnet.tar.gz) | | | **v1.2.2** | `76da1da` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-arm64-mainnet.tar.gz) | | | **v1.2.1** | `7955bb7` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-arm64-mainnet.tar.gz) | | | **v1.2.0** | `47e355b` | 12,004,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-arm64-mainnet.tar.gz) | | | **v1.1.4** | `c795773` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-arm64-mainnet.tar.gz) | | | **v1.1.2** | `3d83aa3` | 3,263,600 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-arm64-mainnet.tar.gz) | | | **v1.1.0** | `17ceaa7` | 1,694,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-arm64-mainnet.tar.gz) | | | **v1.0.0** | `d996084` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-arm64-mainnet.tar.gz) | Genesis | ### 相关文档 * [升级指南](/cn/how-to/upgrade-node) - 分步升级流程 * [主网信息](/cn/reference/mainnet-information) - 当前网络详情 ## 网络路由 为 Stable 上的应用优化连接性和数据传输的网络路由提供商。 ### 概览表 | **提供商** | **类别** | **文档 / 入门** | **说明** | | :---------------------------------- | :----- | :---------------------------------- | :-------------- | | [**Optimum**](https://optimum.xyz/) | 去中心化网络 | [optimum.xyz](https://optimum.xyz/) | 为 dApp 提供的高性能路由 | ### Optimum 一种为速度和可扩展 web3 交互优化的去中心化互联网协议。 **功能** * 高性能去中心化网络 * 更快的应用数据路由 * 为 dApp 提供可靠的基础设施 **入门**:访问 [optimum.xyz](https://optimum.xyz/) 了解如何通过 Optimum 的去中心化网络基础设施路由您的 Stable dApp 流量。 *** 拥有与 Stable 的网络集成?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## 网络升级 本指南涵盖 Stable 节点的所有配置选项,包括针对不同使用场景的优化。 ### 配置文件概述 Stable 节点使用两个主要配置文件: * **`config.toml`**:核心 StableBFT 配置 * **`app.toml`**:应用程序专属配置 这两个文件都位于 `~/.stabled/config/` ### 核心配置 (config.toml) #### 基本设置 :::code-group ```toml [Mainnet] # The ID of the chain to join chain_id = "stable_988-1" # A custom human-readable name for this node moniker = "your-node-name" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb db_backend = "goleveldb" ``` ```toml [Testnet] # The ID of the chain to join chain_id = "stabletestnet_2201-1" # A custom human-readable name for this node moniker = "your-node-name" # Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb db_backend = "goleveldb" ``` ::: #### P2P 配置 :::code-group ```toml [Mainnet] [p2p] # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial external_address = "YOUR_PUBLIC_IP:26656" # Comma separated list of seed nodes seeds = "17a539fda42863a99755547e1c9b3ec4c38a4439@seed1.stable.xyz:26656" # Comma separated list of persistent peers persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656" ``` ```toml [Testnet] [p2p] # Address to listen for incoming connections laddr = "tcp://0.0.0.0:26656" # Address to advertise to peers for them to dial external_address = "YOUR_PUBLIC_IP:26656" # Comma separated list of seed nodes seeds = "39e061b167162f6621ddadcf1be21d6fa585a468@seed1.testnet.stable.xyz:26656" # Comma separated list of persistent peers persistent_peers = "5ed0f977a26ccf290e184e364fb04e268ef16430@peer1.testnet.stable.xyz:26656" ``` ::: 其他 P2P 设置(两个网络相同): ```toml # Maximum number of inbound peers max_num_inbound_peers = 50 # Maximum number of outbound peers max_num_outbound_peers = 30 # Toggle to disable guard against peers connecting from the same ip allow_duplicate_ip = false # Peer connection configuration handshake_timeout = "20s" dial_timeout = "3s" # Time to wait before flushing messages out on the connection flush_throttle_timeout = "100ms" # Maximum size of a message packet payload max_packet_msg_payload_size = 1024 # Rate limiting send_rate = 5120000 # 5 MB/s recv_rate = 5120000 # 5 MB/s # Seed mode (for seed nodes only) seed_mode = false # Enable peer exchange reactor pex = true ``` #### RPC 服务器配置 ```toml [rpc] # TCP or UNIX socket address for the RPC server laddr = "tcp://127.0.0.1:26657" # A list of origins a cross-domain request can be executed from cors_allowed_origins = ["*"] # A list of methods the client is allowed to use with cross-domain requests cors_allowed_methods = ["HEAD", "GET", "POST"] # A list of non simple headers the client is allowed to use with cross-domain requests cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] # TCP or UNIX socket address for the gRPC server grpc_laddr = "tcp://127.0.0.1:9090" # Maximum number of simultaneous connections grpc_max_open_connections = 900 # Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool unsafe = false # Maximum number of simultaneous connections (including WebSocket) max_open_connections = 900 # Maximum number of unique clientIDs that can connect max_subscription_clients = 100 # Maximum number of unique queries a given client can subscribe to max_subscriptions_per_client = 5 # How long to wait for a tx to be committed timeout_broadcast_tx_commit = "10s" # Maximum size of request body max_body_bytes = 1000000 # Maximum size of request header max_header_bytes = 1048576 ``` #### 内存池配置 ```toml [mempool] # Mempool version to use version = "v1" # Recheck enabled recheck = true # Broadcast enabled broadcast = true # Maximum number of transactions in the mempool size = 3000 # Limit the total size of all txs in the mempool max_txs_bytes = 1073741824 # 1GB # Size of the cache cache_size = 10000 # Do not remove invalid transactions from the cache keep-invalid-txs-in-cache = false # Maximum size of a single transaction max_tx_bytes = 1048576 # 1MB # Maximum size of a batch of transactions to send to a peer max_batch_bytes = 0 ``` #### 共识配置 ```toml [consensus] # How long we wait for a proposal block before prevoting nil timeout_propose = "5s" # How much timeout_propose increases with each round timeout_propose_delta = "10ms" # How long we wait after receiving +2/3 prevotes timeout_prevote = "150ms" # How much the timeout_prevote increases with each round timeout_prevote_delta = "10ms" # How long we wait after receiving +2/3 precommits timeout_precommit = "150s" # How much the timeout_precommit increases with each round timeout_precommit_delta = "10ms" # Make progress as soon as we have all the precommits skip_timeout_commit = false # Enable/disable double sign check double_sign_check_height = 2 # EmptyBlocks mode create_empty_blocks = true create_empty_blocks_interval = "0s" # Reactor sleep duration peer_gossip_sleep_duration = "100ms" peer_query_maj23_sleep_duration = "2s" ``` ### 应用程序配置 (app.toml) #### 基本应用程序设置 ```toml # Pruning strategy pruning = "default" # HaltHeight contains a non-zero block height at which a node will halt halt-height = 0 # HaltTime contains a non-zero time at which a node will halt halt-time = 0 # MinRetainBlocks defines the number of blocks for which a node will retain min-retain-blocks = 0 # InterBlockCache enables inter-block caching inter-block-cache = true # IndexEvents defines the set of events in the form {eventType}.{attributeKey} index-events = [] # IavlCacheSize set the size of the iavl tree cache iavl-cache-size = 781250 ``` #### API 配置 ```toml [api] # Enable defines if the API server should be enabled enable = true # Swagger defines if swagger documentation should automatically be registered swagger = true # Address defines the API server to listen on address = "tcp://0.0.0.0:1317" # MaxOpenConnections defines the number of maximum open connections max-open-connections = 1000 # EnabledUnsafeCORS defines if CORS should be enabled enabled-unsafe-cors = true ``` #### gRPC 配置 ```toml [grpc] # Enable defines if the gRPC server should be enabled enable = true # Address defines the gRPC server address to bind to address = "0.0.0.0:9090" ``` #### EVM JSON-RPC 配置 ```toml [json-rpc] # Enable the JSON-RPC server enable = true # Address to bind the JSON-RPC server address = "0.0.0.0:8545" # Address to bind the WebSocket server ws-address = "0.0.0.0:8546" # APIs to enable api = "eth,net,web3,txpool,personal,debug" # Gas cap for eth_call/estimateGas gas-cap = 25000000 # EVM timeout for eth_call/estimateGas evm-timeout = "5s" # Tx fee cap for transactions txfee-cap = 1 # Filter cap for eth_getLogs filter-cap = 200 # FeeHistory cap feehistory-cap = 100 # Block range cap for eth_getLogs logs-cap = 10000 # Block range cap block-range-cap = 10000 # HTTP timeout http-timeout = "30s" # HTTP idle timeout http-idle-timeout = "120s" # Allow unprotected transactions allow-unprotected-txs = true # Maximum number of transactions in the pool max-tx-in-pool = 3000 # Enable indexer enable-indexer = false # Enable metrics metrics = true ``` ### 配置方案 #### 全节点(默认) 适用于全节点的均衡配置: ```bash # config.toml adjustments sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 50/' ~/.stabled/config/config.toml sed -i 's/^max_num_outbound_peers = .*/max_num_outbound_peers = 30/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^pruning = ".*"/pruning = "default"/' ~/.stabled/config/app.toml sed -i 's/^snapshot-interval = .*/snapshot-interval = 1000/' ~/.stabled/config/app.toml ``` #### 归档节点 不进行修剪,保留完整历史记录: ```bash # config.toml adjustments sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^pruning = ".*"/pruning = "nothing"/' ~/.stabled/config/app.toml ``` #### RPC 节点 公共 RPC 端点配置: ```bash # config.toml adjustments sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 30/' ~/.stabled/config/config.toml sed -i 's/^max_open_connections = .*/max_open_connections = 30/' ~/.stabled/config/config.toml sed -i 's/^cors_allowed_origins = .*/cors_allowed_origins = ["*"]/' ~/.stabled/config/config.toml # app.toml adjustments sed -i 's/^enable = .*/enable = true/' ~/.stabled/config/app.toml sed -i 's/^swagger = .*/swagger = true/' ~/.stabled/config/app.toml sed -i 's/^enabled-unsafe-cors = .*/enabled-unsafe-cors = true/' ~/.stabled/config/app.toml ``` ### 监控配置 #### Prometheus 指标 ```toml # config.toml [instrumentation] # Enable Prometheus metrics prometheus = true # Metrics listen address prometheus_listen_addr = ":26660" # Namespace for metrics namespace = "stablebft" ``` #### 日志记录 ```toml # config.toml [log] # Log level (trace|debug|info|warn|error|fatal|panic) level = "info" # Log format (plain|json) format = "plain" ``` ### 应用配置更改 进行配置更改后: ```bash # Restart the node sudo systemctl restart ${SERVICE_NAME} # Check logs for errors sudo journalctl -u ${SERVICE_NAME} -f # Verify configuration loaded curl localhost:26657/status | jq '.result.node_info' ``` ### 后续步骤 * 为您的节点[设置监控](/cn/how-to/monitor-node) * 查阅[故障排除指南](/cn/how-to/troubleshoot-node)以了解常见问题 ## 运维 运维涵盖运行 Stable 节点的方方面面:全节点或归档节点、测试网或主网,从安装到监控。关于您的节点所强制执行的链级行为(费用模型、最终性、USDT0 作为 gas),请参阅 [Gas 定价](/cn/explanation/gas-pricing)、[最终性](/cn/explanation/finality) 和 [架构概览](/cn/explanation/core-optimization-overview)。 ### 快速链接 * **[系统要求](/cn/reference/node-system-requirements)** - 不同节点类型的硬件和软件要求 * **[安装指南](/cn/how-to/install-node)** - 各种平台的分步安装说明 * **[配置](/cn/reference/node-configuration)** - 详细的配置选项和最佳实践 * **[快照与同步](/cn/how-to/use-node-snapshots)** - 使用快照的快速同步选项 * **[创建验证者](/cn/how-to/run-validator)** - 将已同步的节点注册为验证者并进行自委托 * **[升级指南](/cn/how-to/upgrade-node)** - 节点升级流程和版本历史 * **[监控](/cn/how-to/monitor-node)** - 节点监控的工具和指标 * **[故障排查](/cn/how-to/troubleshoot-node)** - 常见问题和解决方案 如需从链上读取验证者数据(质押、在线时长、投票历史)而不运行 `stabled`,请参阅 [索引验证者数据](/cn/how-to/index-validator-data)。 ### 节点类型 #### 全节点 全节点维护区块链的完整副本,并验证所有交易和区块。全节点: * 验证所有交易和区块 * 维护整个区块链历史 * 可以向其他节点提供数据 * 支持网络的去中心化 #### 归档节点 归档节点存储所有状态的完整历史,并能提供历史查询。归档节点: * 存储所有历史状态 * 支持在任意区块高度进行历史查询 * 需要显著更多的存储空间 * 对区块浏览器和分析工具至关重要 ### 网络信息 如需完整的网络详情,包括 RPC 端点、区块浏览器和链参数,请参阅: * **[主网](/cn/reference/mainnet-information)** - 主网详情 * **[测试网](/cn/reference/testnet-information)** - 测试网详情 ### 支持与社区 * **Discord**:[加入 Stable Discord](https://discord.gg/stablexyz) ### 快速开始 面向希望快速上手的经验丰富的运维人员: 1. 查看 [系统要求](/cn/reference/node-system-requirements) 2. 遵循 [安装指南](/cn/how-to/install-node) 3. 使用 [配置指南](/cn/reference/node-configuration) 配置您的节点 4. 通过 [快照](/cn/how-to/use-node-snapshots) 加快同步 5. 使用 [监控指南](/cn/how-to/monitor-node) 监控您的节点 如需了解网络参数和 RPC 端点,请参阅 [主网信息](/cn/reference/mainnet-information) 或 [测试网信息](/cn/reference/testnet-information)。 ### 节点运维如何与链关联 运行节点意味着强制执行 Stable 的链级规则。以下页面解释了您的节点所实现的行为: * **[合约概览](/cn/explanation/contracts-overview)** 涵盖费用模型、JSON-RPC 接口以及您的节点所服务的系统模块。 * **[最终性](/cn/explanation/finality)** 解释单槽最终性以及在共识层"已确认"的含义。 * **[架构概览](/cn/explanation/core-optimization-overview)** 详细介绍共识、执行、数据库和 RPC 各层。 * **[Gas 定价](/cn/explanation/gas-pricing)** 解释以 USDT0 计价的费用如何定价和收取。 本页面概述了运行不同类型 Stable 节点的硬件和软件要求。 ### 硬件要求 #### 全节点(最低要求) | 组件 | 要求 | 说明 | | -------- | ----------------------------- | ------------------------------- | | **CPU** | 4 核心 | AMD Ryzen 5 / Intel Core i5 或更佳 | | **RAM** | 8 GB | 建议 16 GB 以获得更佳性能 | | **存储** | 500 GB NVMe/SSD | 需要写入吞吐量 > 1000 MiBps | | **网络** | 100 Mbps | 稳定、低延迟连接 | | **操作系统** | Ubuntu 22.04/24.04, Debian 12 | 需要 64 位 Linux | #### 全节点(推荐配置) | 组件 | 要求 | 说明 | | -------- | ------------ | ------------------------------- | | **CPU** | 8 核心 | AMD Ryzen 7 / Intel Core i7 或更佳 | | **RAM** | 16 GB | 32 GB 可获得最佳性能 | | **存储** | 1 TB NVMe | 写入吞吐量 > 2000 MiBps | | **网络** | 1 Gbps | 首选专用连接 | | **操作系统** | Ubuntu 24.04 | 建议最新 LTS | #### 存档节点 | 组件 | 要求 | 说明 | | -------- | ------------ | --------------------------------- | | **CPU** | 16 核心 | AMD Ryzen 9 / Intel Core i9 或同等产品 | | **RAM** | 32 GB | 建议 64 GB | | **存储** | 4 TB NVMe | 快速增长,需规划扩容 | | **网络** | 1 Gbps | 需要不计量连接 | | **操作系统** | Ubuntu 24.04 | 建议最新 LTS | ### 软件要求 #### 操作系统 ##### 支持的发行版 * **Ubuntu 24.04 LTS**(推荐) * **Ubuntu 22.04 LTS** * **Debian 12 (Bookworm)** ##### 系统依赖 ```bash # 更新系统包 sudo apt update && sudo apt upgrade -y # 安装基本工具 sudo apt install -y \ build-essential \ git \ wget \ curl \ jq \ lz4 \ zstd \ htop \ net-tools \ ufw ``` ### 网络要求 #### 带宽使用情况 | 节点类型 | 下载 | 上传 | 月流量 | | ---- | ------------- | ------------ | ------- | | 全节点 | \~50 Mbps 平均 | \~25 Mbps 平均 | \~15 TB | | 存档节点 | \~100 Mbps 平均 | \~50 Mbps 平均 | \~30 TB | ### 云服务提供商推荐 #### AWS * **全节点**:t3.xlarge 或 c5.xlarge * **存档节点**:m5.2xlarge 或 c5.2xlarge * **存储**:gp3 配置 IOPS #### Google Cloud * **全节点**:n2-standard-4 * **存档节点**:n2-standard-8 * **存储**:pd-ssd 或 pd-extreme #### Azure * **全节点**:Standard\_D4s\_v5 * **存档节点**:Standard\_D8s\_v5 * **存储**:Premium SSD v2 #### DigitalOcean * **全节点**:General Purpose 8GB * **存档节点**:CPU-Optimized 16GB * **存储**:Volume Block Storage ### 监控要求 对于生产环境部署,请确保您拥有: * **Prometheus**:用于指标收集 * **Grafana**:用于可视化 * **AlertManager**:用于告警 * **Node Exporter**:用于系统指标 * **日志聚合**:建议使用 ELK 或 Loki ### 安全考虑 #### 系统加固 * 保持操作系统和软件包更新 * 配置自动安全更新 * 仅使用 SSH 密钥(禁用密码认证) * 配置 fail2ban * 启用防火墙(UFW/iptables) * 定期安全审计 ### 安装前检查清单 在进行安装之前,请验证: * [ ] 硬件满足最低要求 * [ ] 操作系统受支持且已更新 * [ ] 存储具有足够的 IOPS * [ ] 网络带宽充足 * [ ] 防火墙规则已配置 * [ ] 系统监控已设置 * [ ] 备份策略已定义 * [ ] 安全措施已到位 ## 预言机 预言机为智能合约提供链下数据,例如资产价格。RedStone 在 Stable 上运营价格喂送。 ### 概览表 | **提供商** | **类别** | **支持的交易对** | **文档 / 入门** | **备注** | | :-------------------------------------------- | :------ | :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- | :----- | | [**RedStone**](https://www.redstone.finance/) | 预言机价格喂送 | BTC, ETH, USDT, USDC, PYUSD, XAUt, frxUSD, FXS, LBTC, sfrxETH/ETH, sfrxUSD, SolvBTC, sthUSD, thBILL, weETH | [https://docs.redstone.finance/docs/dapps/redstone-push/](https://docs.redstone.finance/docs/dapps/redstone-push/) | 主网已上线 | ### RedStone RedStone 通过其 [Push 模型](https://docs.redstone.finance/docs/dapps/redstone-push/) 在 Stable 上提供预言机价格喂送。喂送合约暴露了与 Chainlink 兼容的 `AggregatorV3Interface`。 **能力** * 基于推送的价格喂送,具有可配置的偏差阈值和心跳间隔 * 与 Chainlink 兼容的 `AggregatorV3Interface`,包含 `latestRoundData()`、`decimals()` 和 `description()` * 覆盖蓝筹资产、稳定币、LST、LRT 以及收益型 / 基本面定价资产 #### 主网价格喂送地址 来源:[RedStone push 喂送仪表板](https://app.redstone.finance/push-feeds?networks=stable\&testnets=true) | **价格喂送** | **合约地址** | **偏差** | **心跳** | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------- | :----- | :----- | | **BTC / USD** | [0x687103bA8CC2f66C94696182Ef410400Da45fb24](https://stablescan.xyz/address/0x687103bA8CC2f66C94696182Ef410400Da45fb24) | 0.5% | 6h | | **ETH / USD** | [0x457BE3C697c644bF329C2C3ea79EbF1D254d603a](https://stablescan.xyz/address/0x457BE3C697c644bF329C2C3ea79EbF1D254d603a) | 0.5% | 6h | | **USDT / USD** | [0x58264801fadCd8598D3EE993572ADe9cA27F42c8](https://stablescan.xyz/address/0x58264801fadCd8598D3EE993572ADe9cA27F42c8) | 0.5% | 6h | | **USDC / USD** | [0x8ea3C667C264BbdaA1dA7638904b8671F451c7F9](https://stablescan.xyz/address/0x8ea3C667C264BbdaA1dA7638904b8671F451c7F9) | 0.5% | 6h | | **PYUSD / USD** | [0x1c30dA143E97c228102A5cAe3960dBBB41321604](https://stablescan.xyz/address/0x1c30dA143E97c228102A5cAe3960dBBB41321604) | 0.5% | 6h | | **XAUt / USD** | [0xd5E244accc514b56DCAD89897DD44499E7C35a05](https://stablescan.xyz/address/0xd5E244accc514b56DCAD89897DD44499E7C35a05) | 0.5% | 6h | | **frxUSD / USD** | [0xB5197ca89507FE045e8ce9996593D35071915EB7](https://stablescan.xyz/address/0xB5197ca89507FE045e8ce9996593D35071915EB7) | 0.5% | 6h | | **FXS / USD** | [0xC3b182aee94AECeCa39b072942f3Ce4B87465517](https://stablescan.xyz/address/0xC3b182aee94AECeCa39b072942f3Ce4B87465517) | 0.5% | 6h | | **LBTC / USD** | [0x80295Cf12E28f3F943304BFd6C2A2C044e731aaB](https://stablescan.xyz/address/0x80295Cf12E28f3F943304BFd6C2A2C044e731aaB) | 0.5% | 6h | | **sfrxETH / ETH** | [0x29533E113D803ab1967F6CB9495B95DC8C1EA594](https://stablescan.xyz/address/0x29533E113D803ab1967F6CB9495B95DC8C1EA594) | 0.5% | 6h | | **sfrxUSD / FUNDAMENTAL** | [0x71784611831b9566df7301A78bC1B3d29a8737bF](https://stablescan.xyz/address/0x71784611831b9566df7301A78bC1B3d29a8737bF) | 0.5% | 6h | | **SolvBTC / FUNDAMENTAL** | [0x58fa68A373956285dDfb340EDf755246f8DfCA16](https://stablescan.xyz/address/0x58fa68A373956285dDfb340EDf755246f8DfCA16) | 0.01% | 24h | | **sthUSD / FUNDAMENTAL** | [0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db](https://stablescan.xyz/address/0xb81131B6368b3F0a83af09dB4E39Ac23DA96C2Db) | 0.5% | 12h | | **thBILL / FUNDAMENTAL / USD** | [0x7532df197a36587aeD2B9A59785c8BeD182FA62D](https://stablescan.xyz/address/0x7532df197a36587aeD2B9A59785c8BeD182FA62D) | 0.5% | 6h | | **weETH / FUNDAMENTAL** | [0xD57b79401956BE4872D3d03F0C920639335e350F](https://stablescan.xyz/address/0xD57b79401956BE4872D3d03F0C920639335e350F) | 0.5% | 6h | ### 读取价格喂送 喂送合约实现了与 Chainlink 兼容的 [`AggregatorV3Interface`](https://docs.redstone.finance/docs/dapps/redstone-push/)。任何 Chainlink 风格价格喂送所使用的相同消费者模式均适用于此。 ```solidity pragma solidity ^0.8.25; interface AggregatorV3Interface { function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ); function decimals() external view returns (uint8); function description() external view returns (string memory); } contract OracleConsumer { AggregatorV3Interface public oracle; constructor(address oracleAddress) { oracle = AggregatorV3Interface(oracleAddress); } function getLatestPriceData() external view returns ( uint80 roundId, int256 answer, uint256 updatedAt ) { (roundId, answer, , updatedAt, ) = oracle.latestRoundData(); return (roundId, answer, updatedAt); } } ``` :::note Stable 上的 RedStone push 喂送目前仅在主网上可用。在消费价格之前,请始终根据您应用程序的新鲜度容忍度验证链上的 `updatedAt`。 ::: #### 直接读取喂送 您无需部署消费者合约即可读取任何 RedStone 喂送。以下调用读取 ETH/USD 喂送: ```bash cast call 0x457BE3C697c644bF329C2C3ea79EbF1D254d603a "latestRoundData()(uint80,int256,uint256,uint256,uint80)" --rpc-url https://rpc.stable.xyz ``` #### 将消费者部署到 Stable 主网 这假设您已安装 Foundry 并拥有一个有资金的钱包。完整设置说明请参阅[部署智能合约](/cn/tutorial/smart-contract)教程。 1. 将上面的合约保存到 Foundry 项目中的 `src/OracleConsumer.sol`。 2. 使用 BTC/USD 主网喂送地址进行部署: ```bash source .env ; forge create src/OracleConsumer.sol:OracleConsumer --broadcast --rpc-url $STABLE_MAINNET_RPC_URL --private-key $PRIVATE_KEY --constructor-args 0x687103bA8CC2f66C94696182Ef410400Da45fb24 ``` 3. 从您部署的合约中读取最新价格: ```bash cast call "getLatestPriceData()(uint80,int256,uint256)" --rpc-url $STABLE_MAINNET_RPC_URL ``` ### 有预言机想要集成 Stable? 请联系团队 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz),以便在此页面上列出。 ## 发送和接收 USDT0 在 Stable 上,P2P 支付在不到一秒内完成结算。根据使用场景,有两种转账方式可供选择。 ### 原生转账 发送方直接签署交易并将其广播给接收方。这需要消耗 21,000 gas(在 100 gwei 时约为 0.00021 USDT0)。无需与合约交互。 原生转账是最简单的方式:发送方签署一笔将 USDT0 直接发送给接收方的交易。这相当于在任何支付应用中输入金额并选择"发送"。 有关代码演练,请参阅[发送你的第一笔 USDT0](/cn/tutorial/send-usdt0)。 ### 应用发起的转账(ERC-3009) 发送方签署一份链下授权。应用程序或服务商代表他们提交交易。结合 [Gas 豁免](/cn/how-to/integrate-gas-waiver),gas 成本为 0。 [ERC-3009](/cn/explanation/erc-3009) 更适合应用发起的支付(例如 web 应用中的支付),因为它将签署者与提交者分离开来。发送方只需在链下签署授权,由应用程序或服务商处理链上提交。 ### 它的不同之处 在传统支付通道上,一笔 P2P 转账涉及银行处理、清算和结算,可能需要 1–3 个工作日。即使在其他区块链上,发送方也需要在持有支付代币的同时持有波动性的 gas 代币(ETH、SOL)。而在 Stable 上,发送方只需持有 USDT0,gas 可以被豁免,结算在不到一秒内即为最终确认。 | **方面** | **传统(银行转账)** | **其他区块链** | **Stable** | | :----- | :----------- | :--------------------------- | :---------------------------------- | | 结算时间 | 1–3 个工作日 | 数秒到数分钟,取决于链 | 不到 1 秒(单槽最终性) | | 所需资产 | 法定货币 | 支付代币 + 单独的 gas 代币(ETH、SOL 等) | 仅 USDT0(单一资产) | | 交易成本 | 电汇/中介费用 | 可能因网络拥堵而飙升 | 原生转账约 0.00021 USDT0 或通过 Gas 豁免实现 $0 | **另请参阅:** * [发送你的第一笔 USDT0](/cn/tutorial/send-usdt0) * [ERC-3009(带授权转账)](/cn/explanation/erc-3009) * [Gas 豁免](/cn/how-to/integrate-gas-waiver) ## 按 API 请求计费 使用 [x402](/cn/explanation/x402) 中间件,通过每次请求的定价来实现任何 HTTP 端点的货币化。服务器声明价格,客户端按调用付费,结算在请求生命周期内完成。无需账户、无需 API 密钥、无需计费周期。 ### 工作原理 服务器为其希望货币化的端点添加 x402 中间件。当请求未携带付款到达时,服务器返回 HTTP `402 Payment Required` 以及包含价格、代币和网络的 `PAYMENT-REQUIRED` 头。客户端为指定金额签署一个 [ERC-3009](/cn/explanation/erc-3009) 授权并重新提交。Facilitator 在链上结算付款,服务器返回资源。 #### 请求流程 1. 客户端向服务器发送 HTTP 请求。 2. 服务器返回 `402 Payment Required`,并附带包含价格、代币、网络和收款方的 `PAYMENT-REQUIRED` 头。 3. 客户端为指定金额签署 ERC-3009 授权,并带着 `PAYMENT-SIGNATURE` 头重新提交请求。 4. Facilitator 验证签名并在链上结算转账。 5. 服务器返回资源,并附带包含结算收据的 `PAYMENT-RESPONSE` 头。 #### 定价 价格以 USDT0 原子单位(6 位小数)计价。成本参数 `"1000"` 恰好等于 $0.001。成本 `"50000"` 等于 $0.05。这种精度使服务器能够将价格设置为不到一美分的小数额。 #### 基础设施 在 Stable 上,[Semantic Pay](https://x402.semanticpay.io) 运营着一个公共 facilitator。开发者可以将其中间件指向此端点,而无需运行自己的结算基础设施。 x402 为 Express(`@x402/express`)、Hono(`@x402/hono`)和 Next.js(`@x402/next`)提供中间件。所有框架的模式都相同:创建一个 facilitator 客户端,注册 EVM 方案,并应用中间件。 ### 有何不同 传统的 API 货币化需要用户注册、API 密钥管理、用量追踪、计费周期以及支付处理器集成。使用 x402,服务器为每个端点附加一个付款处理器,客户端按请求付费,结算在同一个 HTTP 生命周期内完成。服务器无需知道客户端是谁,只需知道提交了有效付款即可。 | **方面** | **传统方式(API 密钥 + 计费周期)** | **Stable(x402)** | | :-------- | :------------------------ | :----------------- | | 服务端设置 | 注册、API 密钥、用量追踪、计费周期、支付处理器 | 每个端点的 x402 付款处理器 | | 客户端接入 | 账户创建、API 密钥发放 | 无(仅需钱包) | | 计费模式 | 按月或按用量开票 | 按请求结算 | | 是否需要客户端身份 | 是(API 密钥) | 否(仅需有效付款) | | 结算 | 计费周期结束时 | 请求生命周期内(1 秒以内) | | 最低可行价格 | 约 $0.30(卡处理下限) | $0.001(USDT0 原子单位) | | 客户端类型 | 仅限人类用户(需注册) | 任意钱包:人类、AI 代理、脚本 | **另请参阅:** * [x402(HTTP 原生支付)](/cn/explanation/x402) * [ERC-3009(带授权的转账)](/cn/explanation/erc-3009) * [Gas 豁免](/cn/how-to/integrate-gas-waiver) ## 出入金通道 出入金通道合作伙伴将 Stable 连接到全球法定货币系统,使用户和企业能够在 USDT、本地货币和支付通道之间进行转移。 ### 出入金通道概览表 | **服务商** | **类别** | **文档 / 开始使用** | **备注** | | :----------------------------------------------------------------------- | :----- | :----------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | | [**Onmeta**](https://onmeta.in/on-off-ramp) | 出入金通道 | [https://docs.onmeta.in/](https://docs.onmeta.in/) | 合规的法币兑 USDT + 跨境支付 | | [**Halliday**](https://halliday.xyz/) | 出入金通道 | [https://docs.halliday.xyz/pages/home](https://docs.halliday.xyz/pages/home) | CEX + Stripe 集成 | | [**Alchemy Pay**](https://alchemypay.org/about) | 网关 | [https://alchemypay.readme.io/docs/alchemypay-on-ramp](https://alchemypay.readme.io/docs/alchemypay-on-ramp) | 300+ 种支付方式 | | [**DFX**](https://www.dfx.swiss/) | 出金外汇 | [https://docs.dfx.swiss/](https://docs.dfx.swiss/) | 受监管的稳定币兑法币 | | [**Onramp Money**](https://onramp.money/) | 出入金通道 | [https://docs.onramp.money/onramp/](https://docs.onramp.money/onramp/) | 覆盖新兴市场 | | [**MoonPay**](https://www.moonpay.com/business/onramps) | 通用通道 | [https://dev.moonpay.com/docs/on-ramp-overview](https://dev.moonpay.com/docs/on-ramp-overview) | 全球银行卡 + 银行支持 | | [**Transak**](https://transak.com/off-ramp) | 出入金通道 | [https://docs.transak.com/](https://docs.transak.com/) | 450+ 项集成 | | [**Banxa**](https://banxa.com/solutions/by-use-case/on-and-off-ramping/) | 受监管通道 | [https://docs.banxa.com/docs/overview](https://docs.banxa.com/docs/overview) | 覆盖 100+ 个国家的本地通道 | | [**Simplex by Nuvei**](https://www.simplex.com/) | 入金通道 | [https://buy.simplex.com](https://buy.simplex.com) | 在 245+ 个市场支持银行卡、Apple/Google Pay、SEPA、ACH;为包括 Atomic Wallet 在内的下游钱包提供支持 | ### 类别指南 * **出入金通道:** 在法定货币与 Stable 上的 USDT 之间实现直接兑换的平台。 * **支付网关:** 将应用、商户或金融科技公司连接到全球法币通道以实现无缝交易的服务。 * **外汇与出金网络:** 提供合规、大规模稳定币兑法币结算的受监管基础设施。 ### 出入金通道与支付网关 #### MoonPay 全球范围内用于即时购买资产的通用加密货币出入金通道。 **功能** * 银行卡 + 银行 + 本地支付通道 * 快速的全球入驻 * 多链支持 **开始使用**:按照 [MoonPay 入金集成指南](https://dev.moonpay.com/docs/on-ramp-overview) 将 Stable 上的法币兑 USDT 购买功能嵌入到你的应用中。 #### Transak 提供在 450+ 个应用中无障碍买入、卖出和转移加密货币方式的出入金通道。 **功能** * 全球法币方式 * 易于集成的 API * 国家级合规支持 **开始使用**:查看 [Transak 集成文档](https://docs.transak.com/),将 Stable 作为受支持的网络添加出入金通道小组件或 API。 #### Onmeta 为 VDA 平台提供内置合规功能的出入金通道及跨境支付基础设施。 **功能** * 法币 ↔ USDT 兑换 * 合规就绪的流程 * 跨境支付 * 本地结算通道 **开始使用**:参考 [Onmeta API 文档](https://docs.onmeta.in/),在 Stable 上集成法币兑 USDT 兑换和跨境支付。 #### Halliday 端到端支付套件,可实现与顶级 CEX 以及 Coinbase、Stripe 等支付平台的无缝出入金。 **功能** * 直接连接主要 CEX * Stripe + 支付通道集成 * 适合商户和应用的流程 **开始使用**:阅读 [Halliday 集成文档](https://docs.halliday.xyz/pages/home),在你的产品中将基于 CEX 和 Stripe 的支付流程连接到 Stable。 #### Alchemy Pay 全球支付网关,通过 300+ 种支付方式连接传统金融与加密货币。 **功能** * 全球法币出入金通道 * 商户支付 * 银行转账 + 银行卡 + 本地通道 **开始使用**:使用 [Alchemy Pay 入金 API](https://alchemypay.readme.io/docs/alchemypay-on-ramp),为在 Stable 上购买 USDT 添加 300+ 种支付方式。 #### DFX 受瑞士监管的去中心化外汇与出金网络,交易量超过 1 亿笔。 **功能** * 稳定币兑法币兑换 * 受监管的外汇层 * 深度的多币种支持 **开始使用**:查阅 [DFX API 文档](https://docs.dfx.swiss/),使用 Stable 作为源网络设置合规的稳定币兑法币出金流程。 #### Onramp Money 专注于新兴市场的低成本法币兑加密货币网关。 **功能** * 覆盖本地支付方式 * 即时结算 * 130 万+ 受支持用户 **开始使用**:按照 [Onramp Money 开发者指南](https://docs.onramp.money/onramp/),为 Stable 上的新兴市场用户集成法币兑加密货币流程。 #### Banxa 受监管的出入金通道基础设施,通过 100+ 个国家的本地通道将用户连接到加密货币。 **功能** * 受监管的法币网关 * 广泛的全球覆盖 * 银行和本地支付选项 **开始使用**:阅读 [Banxa 集成概览](https://docs.banxa.com/docs/overview),以 Stable 作为受支持的网络启用受监管的出入金通道流程。 #### Simplex by Nuvei 来自 Nuvei 的法币兑加密货币入金通道,在 245+ 个市场支持银行卡、银行和本地支付方式。通过 Simplex 在 Stable 上购买 USDT 也可在任何集成了 Simplex 小组件的钱包或应用内使用,包括 [Atomic Wallet](/cn/reference/wallets#atomic-wallet)。 **功能** * 通过 Visa、Mastercard、Apple Pay、Google Pay、SEPA 和 ACH 在 Stable 上购买 USDT * 覆盖全球 245+ 个市场 * 被下游钱包和应用使用的白标小组件(例如 Atomic Wallet) **开始使用**:前往 [https://buy.simplex.com](https://buy.simplex.com) 直接在 Stable 上购买 USDT,或集成 [Simplex 小组件](https://www.simplex.com/) 以在你自己的应用内提供入金购买服务。 *** 有正在集成 Stable 的出入金通道吗?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## RPC 提供商 支持 Stable 的 RPC 和开发者基础设施提供商。 ### 概览表 | **提供商** | **类别** | **文档 / 开始使用** | **说明** | | :--------------------------------------------------------------------------------------------------------------- | :---------- | :---------------------------------------------------------------------------------------------------------------- | :------------------- | | [**Alchemy**](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC + 开发者平台 | [开始使用 Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC、WebSocket、监控、SDK | | [**Tenderly**](https://tenderly.co/) | 模拟 + 调试 | [tenderly.co](https://tenderly.co/) | 实时模拟、追踪、交易工作流 | ### Alchemy 一个受到全球信赖的完整区块链开发平台。[开始使用 Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) **功能** * RPC + WebSocket 基础设施 * 监控仪表盘 * 开发者 API 和 SDK **开始使用**:在 [Alchemy 仪表盘](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) 中创建一个 Stable 应用以获取 RPC URL,然后将其用作你的 JSON-RPC 端点。 ### Tenderly 一个全栈开发者平台,提供模拟、调试、监控和执行工具。 **功能** * 实时合约模拟 * 调试和追踪 * 面向开发者的交易工作流 **开始使用**:在 [Tenderly 仪表盘](https://tenderly.co/) 中设置一个 Stable 项目,以便为你的合约使用模拟、调试和交易追踪功能。 *** 与 Stable 有 RPC 或基础设施集成吗?请通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系我们。 ## SDK 参考 `@stablechain/sdk` 的完整功能。如需操作指南,请参阅 [SDK 快速开始](/cn/tutorial/sdk-quickstart)。 ### 安装 ```bash npm install @stablechain/sdk viem ``` ```text added 2 packages, audited 3 packages in 2s ``` `viem >= 2.0.0` 是一个 peer dependency。 ### `createStable(config)` 构造一个 `StableClient`。返回一个对象,其方法列在 [`StableClient`](#stableclient) 下。 ```ts import { createStable, Network } from "@stablechain/sdk"; const stable = createStable({ network: Network.Mainnet, account }); ``` ```text StableClient { transfer, quoteBridge, bridge, quoteSwap, swap } ``` #### `StableConfig` | **字段** | **类型** | **默认值** | **描述** | | :------------- | :------------------ | :---------------- | :--------------------------------------- | | `network` | `Network` | `Network.Mainnet` | 目标网络。 | | `rpc` | `string` | `network` 的公共 RPC | RPC 覆盖。 | | `account` | `viem.Account` | | 服务端签名者(例如 `privateKeyToAccount`)。 | | `transport` | `viem.Transport` | | 浏览器钱包传输(例如 `custom(window.ethereum)`)。 | | `walletClient` | `viem.WalletClient` | | 预构建的钱包客户端。优先级高于 `account` 和 `transport`。 | 请提供 `account`、`transport` 或 `walletClient` 中的一个。 ### `StableClient` #### `transfer(params)` 在 Stable 上发送原生 USDT0 或任意 ERC-20。会将钱包切换到 Stable 链,在缺失时从链上获取代币精度,并等待回执。 ```ts const { txHash } = await stable.transfer({ from: "0xYourAddress", to: "0xRecipient", amount: 10, }); ``` ```text { txHash: "0x8f3a...2d41" } ``` | **参数** | **类型** | **描述** | | :-------------- | :-------- | :------------------------ | | `from` | `string` | 发送方地址。 | | `to` | `string` | 接收方地址。 | | `amount` | `number` | 人类可读的金额。 | | `token` | `string?` | ERC-20 合约地址。原生 USDT0 时省略。 | | `tokenDecimals` | `number?` | 精度。省略时从链上获取。 | 返回 `OperationResult`(`{ txHash, toAmount? }`)。 #### `quoteBridge(params)` 预览一次跨链桥接。只读 —— 无需签名,无需 gas。 ```ts const quote = await stable.quoteBridge({ fromChain: Chain.Ethereum, toChain: Chain.Stable, fromToken: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, }); ``` ```text { toAmount: 99.94 } ``` 返回 `BridgeQuote`。 #### `bridge(params)` 跨链桥接代币。SDK 会自动选择路由:USDT0 → USDT0 使用 LayerZero,其他情况使用 LI.FI。传入预先获取的 `quote` 可跳过内部的报价调用。 ```ts const { txHash } = await stable.bridge({ ...bridgeParams, quote }); ``` ```text { txHash: "0xabcd...7890" } ``` | **参数** | **类型** | **描述** | | :------------- | :------------- | :---------------- | | `fromChain` | `Chain` | 源链。 | | `toChain` | `Chain` | 目标链。 | | `fromToken` | `string` | 源代币合约地址。 | | `toToken` | `string` | 目标代币合约地址。 | | `amount` | `number` | 人类可读的金额。 | | `fromDecimals` | `number?` | 源代币精度。默认为 `6`。 | | `recipient` | `string?` | 目标地址。默认为签名者。 | | `quote` | `BridgeQuote?` | 预先获取的报价。跳过内部报价调用。 | #### `quoteSwap(params)` 在 Stable 上获取 LI.FI 兑换报价。返回一个预构建的交易请求和授权地址。 ```ts const quote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, }); ``` ```text { toAmount: 99.81, fromAmount: 100000000n, fromToken: "0x8a2B...", approvalAddress: "0x...", transactionRequest: { ... } } ``` #### `swap(params)` 通过 LI.FI 在 Stable 上兑换代币。自动处理 ERC-20 授权,并在需要时切换钱包所在的链。 ```ts const { txHash, toAmount } = await stable.swap({ ...swapParams, quote }); ``` ```text { txHash: "0xabcd...", toAmount: 99.81 } ``` | **参数** | **类型** | **默认值** | **描述** | | :------------- | :----------- | :------ | :------------------- | | `fromToken` | `string` | | 源代币地址。 | | `toToken` | `string` | | 目标代币地址。 | | `amount` | `number` | | 人类可读的金额。 | | `fromDecimals` | `number?` | `6` | 源代币精度。 | | `toAddress` | `string?` | 签名者 | 接收方地址。 | | `quote` | `SwapQuote?` | | 预先获取的报价。跳过 LI.FI 调用。 | ### 枚举 #### `Network` | **值** | **Chain ID** | | :---------------- | :----------- | | `Network.Mainnet` | 988 | | `Network.Testnet` | 2201 | #### `Chain` 由 `quoteBridge` 和 `bridge` 使用。每一项都有对应的 `CHAIN_CONFIGS` 条目。 | **枚举** | **网络** | **Chain ID** | | :-------------------- | :--------------- | :----------- | | `Chain.Sepolia` | Ethereum Sepolia | 11155111 | | `Chain.StableTestnet` | Stable Testnet | 2201 | | `Chain.Stable` | Stable Mainnet | 988 | | `Chain.Ethereum` | Ethereum | 1 | | `Chain.Arbitrum` | Arbitrum One | 42161 | | `Chain.Ink` | Ink | 57073 | | `Chain.Bera` | Berachain | 80094 | | `Chain.MegaETH` | MegaETH | 4326 | | `Chain.Base` | Base | 8453 | | `Chain.BSC` | BNB Smart Chain | 56 | | `Chain.HyperEVM` | HyperEVM | 999 | ### `CHAIN_CONFIGS` 以 `Chain` 枚举为键的 `Partial>`。每一项都暴露 `id`、`rpc`、`usdt` 和 `decimals`。当你需要某条受支持链上的规范 USDT 地址而又不想硬编码时,可以使用它。 ```ts import { CHAIN_CONFIGS, Chain } from "@stablechain/sdk"; console.log(CHAIN_CONFIGS[Chain.Stable]); ``` ```text { id: 988, rpc: "https://rpc.stable.xyz", usdt: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", decimals: 6 } ``` ### 错误 所有 SDK 错误都继承自 `StableError`,而后者继承自 viem 的 `BaseError`。错误携带结构化的元数据,因此你可以基于 `error.name` 或 `instanceof` 进行分支处理。 | **类** | **抛出时机** | **有用的字段** | | :----------------------- | :------------------------- | :-------------------------------------------- | | `StableValidationError` | 参数验证失败(地址错误、金额非有限值、不支持的链)。 | `field`、`value` | | `StableQuoteError` | 向 LI.FI 发起的报价请求失败。 | `provider`、`httpStatus`、`providerCode`、`body` | | `StableTransactionError` | 链上步骤失败:切换链、授权、发送或回滚。 | `phase`、`txHash`、`chainId`、`revertReason` | | `StableNetworkError` | 底层 HTTP/RPC 调用失败。 | `url` | ```ts import { StableTransactionError } from "@stablechain/sdk"; try { await stable.transfer({ from, to, amount: 1 }); } catch (err) { if (err instanceof StableTransactionError && err.phase === "switch_chain") { // user rejected the chain switch } throw err; } ``` ```text StableTransactionError: transfer: wallet rejected or failed to switch to chain 988 Phase: switch_chain Chain ID: 988 ``` ### 推荐的后续步骤 * [**SDK 快速开始**](/cn/tutorial/sdk-quickstart) —— 安装 SDK 并在测试网上运行你的第一笔转账。 * [**与 viem 一起使用**](/cn/how-to/sdk-with-viem) —— 在私钥、浏览器钱包和预构建签名者之间切换。 * [**与 wagmi 一起使用**](/cn/how-to/sdk-with-wagmi) —— 通过 hooks 将 SDK 接入 React 应用。 ## 质押预编译合约参考 :::note **概念:** 关于质押模块的功能及使用场景,请参阅 [质押模块](/cn/explanation/staking-module)。 ::: ### 摘要 `staking` 预编译合约充当桥梁,使 EVM 环境能够使用 Stable SDK 的 `x/staking` 模块功能。 ### 目录 1. **[概念](#concepts)** 2. **[配置](#configuration)** 3. **[方法](#methods)** 4. **[事件](#events)** ### 概念 在 Stable SDK 的 `x/staking` 模块中,必须在链初始化期间注册绑定面额(bond denom)以用于质押。 验证人和委托人只能使用绑定面额的质押代币。 `staking` 预编译合约会执行额外的检查,以确保验证人或委托人即为调用方。 ### 配置 合约地址和 gas 成本是预定义的。 #### 合约地址 * `0x0000000000000000000000000000000000000800` ### 方法 #### `createValidator` 创建一个验证人。 验证人必须由运营者通过初始委托创建。 对于潜在的委托人,验证人应提供相关信息以及佣金率方案。 委托人可以根据所披露的信息选择验证人来委托其代币,并受市场机制的自然调节。 当验证人成功注册时,会触发 `CreateValidator` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ----------------- | --------------- | ---------------- | | description | Description | 验证人的信息 | | commissionRates | CommissionRates | 验证人获得的质押代币奖励的佣金率 | | minSelfDelegation | uint256 | 验证人的最低自委托金额 | | validatorAddress | address | 验证人的地址 | | pubkey | string | 验证人的公钥 | | value | uint256 | 初始自委托给验证人的质押代币数量 | `Description` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | --------------- | ------ | ---------- | | moniker | string | 验证人的名称 | | identity | string | 验证人的身份标识 | | website | string | 验证人网站的 url | | securityContact | string | 安全联系信息 | | details | string | 验证人的额外描述 | `CommissionRates` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ------------- | ------- | --------------- | | rate | uint256 | 验证人当前收取的佣金率 | | maxRate | uint256 | 最大佣金率(不能设置高于此值) | | maxChangeRate | uint256 | 验证人每天可变更的最大佣金率 | `rate` 应设置为市场可接受的适当值。 * 如果验证人的佣金率较高,委托人的收益较低。 * 如果验证人的佣金率较低,验证人的收益较低,会使运营困难。 由于较高的 `maxRate` 会让委托人担心验证人会收取意外的高佣金率,因此应谨慎设置 `maxRate`。`maxChangeRate` 在初始化后不可更改。 ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果验证人成功注册则为 true | #### `editValidator` 验证人更新其信息。 验证人只能更新信息,但 `CommissionRates` 结构体中的 `maxRate` 和 `maxChangeRate` 等不可更改字段除外。 当验证人成功更新时,会触发 `EditValidator` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ----------------- | ----------- | ---------------- | | description | Description | 验证人的信息 | | validatorAddress | address | 验证人的地址 | | commissionRate | int256 | 验证人获得的质押代币奖励的佣金率 | | minSelfDelegation | int256 | 验证人的最低自委托金额 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果验证人成功更新则为 true | #### `delegate` 委托人设置要委托给验证人的代币数量。 当委托成功完成时,会触发 `Delegate` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------------- | | delegatorAddress | address | 委托人的地址 | | validatorAddress | address | 验证人的地址 | | amount | uint256 | 委托给验证人的质押代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果委托成功完成则为 true | ##### 事件 `newShares` 表示委托人的所有权比例。 即使委托相同数量的代币,计算出的 shares 也可能因时间不同而有所差异。 #### `undelegate` 委托人提取委托给验证人的代币数量。 当解除委托成功完成时,会触发 `Unbond` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------------------ | | delegatorAddress | address | 委托人的地址 | | validatorAddress | address | 验证人的地址 | | amount | uint256 | 希望从验证人处解除委托的质押代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ----------------- | | success | bool | 如果解除委托成功完成则为 true | #### `redelegate` 委托人将委托给某验证人的代币数量重新委托给另一个验证人。 当重新委托成功完成时,会触发 `Redelegate` 事件。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------------- | | delegatorAddress | address | 委托人的地址 | | validatorSrc | string | 源验证人的地址 | | validatorDst | string | 目标验证人的地址 | | amount | uint256 | 用于重新委托的质押代币数量 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ---- | ----------------- | | success | bool | 如果重新委托成功完成则为 true | #### `delegation` 返回委托人与验证人之间的委托信息。 如果未找到委托,则 `shares` 和 `balance` 将为 `0`。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | delegatorAddress | address | 委托人的地址 | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------- | ------- | ----------- | | shares | uint256 | 已委托的 shares | | balance | Coin | 已委托代币的数量和面额 | `Coin` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ------ | ------- | ----- | | denom | string | 奖励的面额 | | amount | uint256 | 奖励的数量 | #### `unbondingDelegation` 返回委托人与验证人之间的解除委托信息。 如果未找到解除委托,则返回空的 `UnbondingDelegationOutput`。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | delegatorAddress | address | 委托人的地址 | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------------------- | ------------------------- | ------- | | unbondingDelegation | UnbondingDelegationOutput | 解除委托的信息 | `UnbondingDelegationOutput` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ---------------- | --------------------------- | ------- | | validatorAddress | address | 验证人的地址 | | delegatorAddress | address | 委托人的地址 | | entries | UnbondingDelegationEntry\[] | 解除委托的条目 | `UnbondingDelegationEntry` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | -------------- | ------ | ------- | | creationHeight | uint64 | 条目的创建高度 | | completionTime | uint64 | 条目的完成时间 | | initialBalance | Coin | 条目的初始余额 | | balance | Coin | 条目的余额 | #### `validator` 返回验证人的信息。 如果未找到验证人,则返回空的 `ValidatorOutput`。 ##### 输入 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | --------- | --------- | ------ | | validator | Validator | 验证人的信息 | `Validator` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ----------------- | ------- | ---------------- | | operatorAddress | address | 验证人的地址 | | consensusPubkey | string | 验证人的公钥 | | jailed | bool | 验证人是否被监禁 | | status | int32 | 验证人的状态 | | tokens | uint256 | 委托给验证人的质押代币数量 | | delegatorShares | uint256 | 委托 shares 的数量 | | description | string | 验证人的描述 | | unbondingHeight | int64 | 验证人解除绑定的高度 | | unbondingTime | int64 | 验证人解除绑定的时间 | | commission | uint256 | 验证人获得的质押代币奖励的佣金率 | | minSelfDelegation | uint256 | 验证人的最低自委托金额 | #### `validators` 返回所有与该状态匹配的验证人。 如果未找到验证人,则返回空的 `ValidatorsOutput`。 在 `x/staking` 模块中声明的状态可以是以下之一: * 0 : "BOND\_STATUS\_UNSPECIFIED",未指定状态 * 1 : "BOND\_STATUS\_UNBONDING",验证人正在解除绑定 * 2 : "BOND\_STATUS\_UNBONDED",验证人已解除绑定 * 3 : "BOND\_STATUS\_BONDED",验证人已绑定 ##### 输入 | 名称 | 类型 | 描述 | | ----------- | ------- | ------ | | status | string | 验证人的状态 | | pageRequest | PageReq | 分页请求 | `PageReq` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ---------- | ----- | --------- | | key | bytes | 页面的键 | | offset | int64 | 页面的偏移量 | | limit | int64 | 页面的限制数量 | | countTotal | bool | 是否统计结果的总数 | | reverse | bool | 是否反转结果 | ##### 输出 | 名称 | 类型 | 描述 | | ------------ | ------------ | ----- | | validators | Validator\[] | 验证人数组 | | pageResponse | PageResp | 分页响应 | `PageResp` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ------- | ------ | ------- | | nextKey | bytes | 页面的下一个键 | | total | uint64 | 结果的总数 | #### `redelegation` 返回委托人、源验证人和目标验证人的重新委托信息。 如果未找到重新委托,则返回空的 `RedelegationOutput`。 ##### 输入 | 名称 | 类型 | 描述 | | ------------------- | ------- | -------- | | delegatorAddress | address | 委托人的地址 | | srcValidatorAddress | address | 源验证人的地址 | | dstValidatorAddress | address | 目标验证人的地址 | ##### 输出 | 名称 | 类型 | 描述 | | ------------ | ------------------ | ------- | | redelegation | RedelegationOutput | 重新委托的信息 | `RedelegationOutput` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | ------------------- | -------------------- | -------- | | delegatorAddress | address | 委托人的地址 | | validatorSrcAddress | address | 源验证人的地址 | | validatorDstAddress | address | 目标验证人的地址 | | entries | RedelegationEntry\[] | 重新委托的条目 | `RedelegationEntry` 是一个具有以下字段的结构体: | 名称 | 类型 | 描述 | | -------------- | ------ | ------- | | creationHeight | uint64 | 条目的创建高度 | | completionTime | uint64 | 条目的完成时间 | | initialBalance | Coin | 条目的初始余额 | | balance | Coin | 条目的余额 | #### `redelegations` 返回委托人、源验证人和目标验证人的所有重新委托。 如果未找到重新委托,则返回空的 `RedelegationResponse` 和 `PageResp`。 ##### 输入 | 名称 | 类型 | 描述 | | ------------------- | ------- | -------- | | delegatorAddress | address | 委托人的地址 | | srcValidatorAddress | address | 源验证人的地址 | | dstValidatorAddress | address | 目标验证人的地址 | | pageRequest | PageReq | 分页请求 | ##### 输出 | 名称 | 类型 | 描述 | | ------------ | ----------------------- | ------- | | response | RedelegationResponse\[] | 重新委托的信息 | | pageResponse | PageResp | 分页响应 | ### 事件 #### CreateValidator | 名称 | 类型 | 是否索引 | 描述 | | -------- | ------- | ---- | ---------------- | | valiAddr | address | Y | 验证人的地址 | | value | uint256 | N | 初始自委托给验证人的质押代币数量 | #### EditValidator | 名称 | 类型 | 是否索引 | 描述 | | ----------------- | ------- | ---- | ------------------- | | valiAddr | address | Y | 验证人的地址 | | commissionRate | int256 | N | 验证人获得的质押代币奖励的更新后佣金率 | | minSelfDelegation | int256 | N | 验证人更新后的最低自委托金额 | #### Delegate | 名称 | 类型 | 是否索引 | 描述 | | ------------- | ------- | ---- | ---------------- | | delegatorAddr | address | Y | 委托人的地址 | | validatorAddr | string | Y | 验证人的地址 | | amount | uint256 | N | 委托给验证人的质押代币数量 | | newShares | uint256 | N | 委托后的委托 shares 数量 | #### Unbond | 名称 | 类型 | 是否索引 | 描述 | | -------------- | ------- | ---- | ---------------- | | delegatorAddr | address | Y | 委托人的地址 | | validatorAddr | string | Y | 验证人的地址 | | amount | uint256 | N | 从验证人处解除委托的质押代币数量 | | completionTime | uint256 | N | 解除委托的完成时间 | #### Redelegate | 名称 | 类型 | 是否索引 | 描述 | | ------------------- | ------- | ---- | ------------- | | delegatorAddr | address | Y | 委托人的地址 | | validatorSrcAddress | address | Y | 源验证人的地址 | | validatorDstAddress | address | Y | 目标验证人的地址 | | amount | uint256 | N | 用于重新委托的质押代币数量 | | completionTime | uint256 | N | 重新委托的完成时间 | ## 设置定期账单 基于拉取的订阅让服务提供商可以按计划收取付款,而无需订阅者主动发起每一笔付款。 此模式由 [EIP-7702](/cn/reference/eip-7702-api) 账户抽象实现。订阅者的 EOA 将执行权委托给一个订阅委托合约,提供商在每个账单周期调用该合约。 订阅者只需操作两次:一次订阅,一次取消。 ### 工作原理 订阅者将其 EOA 委托给一个强制执行账单条款的合约。通过 EIP-7702,订阅者的账户临时获得合约逻辑,使服务提供商能够在每个账单周期收取付款,而无需订阅者每次都签名。 #### 订阅生命周期 1. **委托**:订阅者通过 EIP-7702 将其 EOA 委托给订阅委托合约。 2. **订阅**:订阅者注册账单条款:提供商地址、每个周期的金额以及账单间隔。 3. **收取**:服务提供商在每个账单周期触发收取。委托合约在执行 USDT0 转账前会验证调用者、间隔和金额。 4. **取消**:订阅者撤销订阅或清除委托,以停止未来的收取。 #### 重要注意事项 * **持久委托**:EIP-7702 委托会一直保持,直到订阅者明确更改或清除它。无需在每个账单周期重新委托。 * **每个 EOA 单一委托**:EIP-7702 每个 EOA 只支持一个活动委托。如果订阅者随后委托给另一个合约,订阅委托逻辑将被替换,收取将失败。请使用模块化委托合约,在单一委托下支持多种功能(订阅、批量付款、消费限额)。 * **使用经过审计的委托**:委托合约对订阅者的 EOA 拥有完全执行权。仅委托给经过审计的合约。 ### 它的不同之处 传统订阅会存储卡片数据、重试失败的扣款并管理复杂的账单状态。使用 EIP-7702 订阅时,账单条款由订阅者自己 EOA 上的委托逻辑强制执行。提供商每个周期只能收取约定的金额,订阅者可以随时通过撤销委托来取消。 | **方面** | **传统(卡片存档)** | **Stable** | | :------ | :----------- | :--------------- | | 设置 | 在支付处理商处注册卡片 | 单笔 EIP-7702 委托交易 | | 账单 | 处理商扣取存档卡片 | 提供商调用委托合约 | | 存储的支付数据 | 处理商持有卡号、CVV | 链下不存储任何支付凭证 | | 取消 | 联系提供商或发卡机构 | 订阅者在链上撤销委托 | | 超额扣款风险 | 取决于提供商端的账单控制 | 账单条款由合约强制执行 | **另请参阅:** * [EIP-7702](/cn/reference/eip-7702-api) * [ERC-3009(带授权转账)](/cn/explanation/erc-3009) ## 系统模块参考 Stable 通过 **系统模块(System Modules)** 暴露核心结算行为,这些模块以 **预编译合约(precompiled contracts)** 的形式实现,以提升 gas 效率并提供可预测的控制。 :::note **概念:** 关于系统模块的作用以及为何将它们暴露为预编译合约,请参阅 [系统模块](/cn/explanation/system-modules-overview)。 ::: **核心模块:** * [Bank 模块](/cn/reference/bank-module-api) * 处理 USDT 转账、余额记账和托管流程 * [Distribution 模块](/cn/reference/distribution-module-api) * 面向网络参与者的费用分配和奖励逻辑 * [Staking 模块](/cn/reference/staking-module-api) * 控制验证者参与和质押(随主网推出) **dApp 可以利用内置模块,而无需重新实现代币或结算逻辑。** ## 系统交易参考 :::note **概念:** 关于系统交易如何将 SDK 事件桥接到 EVM 以及为什么这很重要,请参阅[系统交易](/cn/explanation/system-transactions)。 ::: ### 摘要 系统交易为 Stable 协议提供了一种为 Stable SDK 操作发出 EVM 事件的方式。当诸如解绑完成之类的质押事件在 SDK 层发生时,协议会自动生成发出相应事件的 EVM 交易。这使得这些操作对 EVM 工具和应用程序完全可见。 ### 动机 你和你在 Stable 上的应用程序期望通过标准的 EVM 接口(如 `eth_getLogs`)来监控区块链事件。但关键操作发生在 Stable SDK 模块中,而这些模块本身不会自然地发出 EVM 事件。这造成了可见性缺口:EVM dApp 无法轻松追踪用户的代币何时完成解绑。 系统交易弥补了这一缺口。当质押模块完成一个解绑操作时,Stable 的 x/stable 模块会检测到该事件并生成一个调用 StableSystem 预编译(`0x0000000000000000000000000000000000009999`)的系统交易。然后,预编译会发出任何 dApp 都可以订阅的正确 EVM 事件。系统交易以一个特殊的发送者地址(`0x8888888888888888888888888888888888888888`)运行,只有协议才能使用该地址。这可以防止任何人伪造协议事件,同时保持事件发出过程无需信任且可在链上验证。 ### 规范 系统交易通过三个主要组件运作:x/stable 模块的 EndBlocker、PrepareProposal 处理器以及 StableSystem 预编译。 #### 架构概览 system-transaction-architecture #### StableSystem 预编译 StableSystem 预编译位于 `0x0000000000000000000000000000000000009999`,处理需要发出 EVM 事件的协议级操作。目前它支持解绑完成通知。 ```solidity interface IStableSystem { /// @notice Processes queued unbonding completions and emits EVM events /// @param blockHeight The block height at which to process completions /// @dev Only callable by system transactions (from = 0x8888888888888888888888888888888888888888) /// @dev Processes up to 100 completions per call /// @dev Automatically deletes processed completions from the queue function notifyUnbondingCompletions(int64 blockHeight) external; /// @notice Emitted when an unbonding operation completes /// @param delegator The address that delegated the tokens /// @param validator The validator address the tokens were delegated to /// @param amount The amount of tokens that finished unbonding (in uusdc) event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); /// @notice The caller is not authorized (not system transaction sender) error Unauthorized(); } ``` #### 系统交易发送者 系统交易使用 `0x8888888888888888888888888888888888888888` 作为发送者地址。该地址: * 不需要签名验证 * 只能由在 PrepareProposal 中创建的交易使用 * 无法被用户或合约伪造 * 通过 SystemTxDecorator ante 处理器跳过手续费扣除 EVM 通过检查 `msg.sender == 0x8888888888888888888888888888888888888888` 来识别系统交易。预编译可以利用这一点来限制仅协议可执行的操作。 #### 事件驱动流程 当用户的解绑期完成时,会发生以下情况: 1. **Stable SDK 层:** 质押模块的 EndBlocker 完成解绑,并发出 EventTypeCompleteUnbonding,其中包含委托人地址、验证者地址和金额。 2. **检测:** x/stable 模块的 EndBlocker 在质押之后运行,并扫描区块事件日志中的解绑事件。对于每个完成的解绑,它都会在状态中排入一个条目,包含委托人地址、验证者地址、金额和区块高度。 3. **系统交易生成**:在下一个区块的 PrepareProposal 中,应用查询所有排队的完成项。如果存在任何完成项,它会创建一个调用 StableSystem.notifyUnbondingCompletions(blockHeight) 的系统交易,并传入当前区块高度。该交易位于区块的最前面,在任何用户交易之前。 4. **执行:** 在区块执行期间,系统交易最先运行。预编译查询该区块高度下排队的完成项的状态,为每一项(最多 100 项)发出一个 UnbondingCompleted 事件,并将它们从队列中删除。 5. **EVM 可见性:** 这些事件出现在交易收据和日志中,可被 eth\_getLogs 查询、区块浏览器以及任何监控 StableSystem 预编译的应用程序看到。 #### 批处理 为了防止区块变得过大,系统每个区块最多处理 100 个解绑完成项。如果有 150 个完成项排队: * 区块 N:创建处理完成项 0-99 的系统交易 * 区块 N+1:创建处理完成项 100-149 的系统交易 预编译直接查询状态,而不是在 calldata 中接收完成数据。这使得交易大小可预测,并将数据从昂贵的 calldata 转移到更便宜的状态读取。 ### 使用示例 最常见的用例是一个质押仪表盘,需要在用户的解绑期完成时通知用户。以下是如何为解绑完成设置监听器。 ```javascript import { ethers } from 'ethers'; // StableSystem precompile address const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999'; // ABI for the UnbondingCompleted event const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; // Connect to the Stable network const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz'); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); // Subscribe to all unbonding completions stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => { console.log('Unbonding completed!'); console.log('Delegator:', delegator); console.log('Validator:', validator); console.log('Amount:', ethers.formatEther(amount), 'tokens'); console.log('Block:', event.log.blockNumber); console.log('Tx Hash:', event.log.transactionHash); }); ``` 每当任何用户的解绑完成时,此监听器都会触发。对于生产环境的 dApp,请按如下所示为特定用户过滤事件。 #### 为特定用户过滤事件 要仅接收特定委托人地址的事件,请使用索引事件参数来创建过滤器: ```javascript // Only watch unbondings for a specific user const userAddress = '0xabcd...'; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { // This only fires for the specified user's unbondings showNotification(`Your unbonding of ${ethers.formatEther(amount)} tokens completed!`); refreshUserBalance(userAddress); }); ``` 如果你正在构建一个特定于验证者的仪表盘,也可以按验证者过滤: ```javascript // Watch all unbondings from a specific validator const validatorAddress = '0x1234...'; const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### 查询历史事件 如果你的 dApp 需要显示过去解绑完成的历史记录,你可以使用带有区块范围的事件过滤器来查询历史事件: ```javascript // Get all unbondings for a user in the last 1000 blocks const currentBlock = await provider.getBlockNumber(); const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter( filter, currentBlock - 1000, currentBlock ); const unbondingHistory = events.map(event => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash })); console.log('Recent unbondings:', unbondingHistory); ``` ### 集成指南 #### 步骤 1:添加 Stable System 合约接口 首先,将 StableSystem 预编译接口添加到你的项目中。如果你使用的是 Foundry 或 Hardhat,请创建一个新的接口文件: ```solidity interface IStableSystem { event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); } ``` 如果你正在构建一个没有 Solidity 合约的纯前端 dApp,你只需要事件的 ABI 片段: ```javascript const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; ``` #### 步骤 2:设置事件监听器 初始化你的 ethers.js provider,并创建一个指向 StableSystem 预编译地址的合约实例。该预编译在 Stable 测试网和 Stable 主网上始终部署于 `0x00000000000....0000009999`。 *注意:该预编译尚未部署在 Stable 主网上,将在 v1.2.0 升级后提供。* ```javascript const provider = new ethers.JsonRpcProvider(RPC_URL); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); ``` #### 步骤 3:在应用程序逻辑中处理事件 订阅事件并相应地更新你的应用程序状态。常见的模式包括: * **余额更新**:当解绑完成时,刷新用户的代币余额 * **通知系统**:当用户的解绑完成时显示弹窗通知 * **仪表盘统计**:实时更新质押指标和图表 * **交易历史**:将已完成的解绑添加到用户的活动动态中 #### 步骤 4:处理连接问题 由于事件订阅依赖于持久的 websocket 连接,请为生产环境的 dApp 实现重连逻辑: ```javascript let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function setupEventListener() { const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz'); provider.on('error', (error) => { console.error('Provider error:', error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); stableSystem.on('UnbondingCompleted', handleUnbonding); } ``` ### 为什么采用这种方法? #### 与自定义索引器相比 以前,Stable SDK 要求你运行自定义索引器来监视 SDK 事件并将它们存储在数据库中。这增加了运维开销并引入了潜在的故障点。 有了系统交易,就不再需要单独的索引器基础设施。事件通过 EVM 的日志系统原生可用,而每个 RPC 节点都已经对其进行了索引和服务。任何标准的 web3 库都可以订阅这些事件,而无需额外的工具。 #### 与轮询 SDK 端点相比 如果没有系统交易,EVM dApp 将需要定期调用 Stable SDK REST 端点来检查解绑期是否已完成。这会带来几个问题: * **延迟增加**:5-10 秒的轮询间隔意味着用户可能要等待那么长时间才能看到更新 * **负载更高**:每个 dApp 实例轮询端点都会增加 RPC 基础设施的负载 * **复杂性**:dApp 需要同时处理 web3 provider(用于 EVM 交互)和 Stable SDK REST 客户端(用于 SDK 查询) * **没有实时更新**:轮询本质上无法提供即时通知 系统交易通过 dApp 已经用于 EVM 交互的相同 websocket 连接提供实时事件通知。这简化了开发者体验并降低了基础设施成本。 ### 安全保证 #### 无需信任的事件发出 系统交易在 `PrepareProposal` ABCI 阶段创建,只有验证者才能执行该阶段。用户提交的交易无法伪造系统发送者地址(`0x8888888888888888888888888888888888888888`)。EVM 的状态转换逻辑强制规定只有发往 StableSystem 预编译地址的交易才能跳过签名验证。 这意味着: * 用户无法伪造解绑完成事件 * 用户无法从自己的交易中调用 `notifyUnbondingCompletions` * 发出 `UnbondingCompleted` 事件的唯一方式是 Stable SDK 质押模块中实际完成一次解绑 #### 没有额外的信任假设 系统交易不会在区块链共识所需的基础上引入新的安全假设。如果你信任验证者正确地执行区块,你就可以信任系统交易事件准确地反映了 Stable SDK 的状态变化。 事件发出过程是确定性的:给定 `EndBlock` 中相同的 SDK 事件,所有诚实的验证者都会在 `PrepareProposal` 期间生成相同的系统交易。共识机制确保验证者就要包含哪些系统交易达成一致。 #### 区块最终性 Stable 区块链通过 StableBFT 的共识机制实现快速最终性。一旦区块被提交,它立即成为最终状态且无法重组。这意味着一旦你收到 `UnbondingCompleted` 事件,你就可以信任它是永久性的。 无需像在概率最终性链上那样等待多次确认。dApp 可以在收到事件后立即更新用户余额并显示通知。 ### 性能与限制 #### 批处理大小约束 每个区块通过系统交易最多处理 100 个解绑完成项。这一限制的存在是为了防止在解绑活动高峰期间区块大小无限制增长。 实际上,假设平均出块时间为 0.7 秒,每个区块 100 个完成项可提供每分钟约 9000 个完成项的吞吐量。正常的质押活动很少达到这一限制。在特殊情况下,完成项可能会排队数个区块才能完全处理。 #### Gas 消耗 系统交易在执行期间消耗 gas,这会计入区块的 gas 限制中。gas 成本与处理的完成项数量呈线性关系: * 基础函数调用:约 21,000 gas * 每个事件发出:约 3,000 gas * 读取状态:每个完成项约 2,000 gas 一个完整的 100 个完成项批次消耗约 521,000 gas。由于 Stable 的区块 gas 限制为 100,000,000,这还不到可用区块空间的 0.6%。 #### 通知延迟 当解绑期在区块 N 期间完成时: 1. Stable 模块的 `EndBlock` 在区块 N 的状态中将完成项排队 2. 区块 N+1 的 `PrepareProposal` 创建一个系统交易 3. 系统交易在区块 N+1 期间执行,发出事件 这意味着在解绑完成和 EVM 事件被发出之间存在一个区块的延迟(约 0.7 秒)。对于大多数用例来说,这一延迟是可以接受的,因为解绑期本身就是 7 天。 #### 高负载场景 如果解绑完成项的到达速度快于每个区块 100 个,它们会在队列中累积。队列按 FIFO 顺序处理,因此最旧的完成项总是最先被通知。 在持续的高负载期间,队列可能会暂时增长。然而,一旦峰值消退,后续完成项较少的区块将逐渐排空队列。该系统旨在处理突发情况而不丢失事件。 ### 未来扩展 系统交易机制为将任何 Stable SDK 操作桥接到 EVM 事件空间提供了一种通用模式。虽然目前仅用于解绑完成,但该架构可以扩展以涵盖更多用例: #### 质押操作 除了解绑之外,其他质押事件也可以发出 EVM 通知: * 验证者更改佣金率 * 验证者监禁和解除监禁 #### 治理执行 当治理提案通过并执行时,系统交易可以发出带有提案 ID 和执行结果的事件。这将允许 dApp 对参数更改或升级做出反应,而无需轮询治理模块。 #### 通用事件桥 该模式可以泛化为一个可配置的事件桥,其中每个模块注册哪些 SDK 事件应被镜像到 EVM。这将提供对所有 Stable SDK 操作的全面可见性,而无需为每个模块定制逻辑。关键的架构原则是系统交易始终是协议级功能,仅由验证者在区块提议期间创建。 ## 生态系统 在本文档中,您可以找到桥接(LayerZero)和 USDT0 的信息。 ### Stable 测试网上的 LayerZero | 名称 | 值 | | ----------------- | ------------------------------------------ | | eid | 40374 | | chainKey | stable-testnet | | stage | testnet | | endpointV2View | 0x6Ac7bdc07A0583A362F1497252872AE6c0A5F5B8 | | endpointV2 | 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 | | sendUln302 | 0x9eCf72299027e8AeFee5DC5351D6d92294F46d2b | | receiveUln302 | 0xB0487596a0B62D1A71D0C33294bd6eB635Fc6B09 | | blockedMessageLib | 0xa229b65cc2191bf60bc24efcda3487d7b5c0c9f0 | | executor | 0x701f3927871EfcEa1235dB722f9E608aE120d243 | | deadDVN | 0xC1868e054425D378095A003EcbA3823a5D0135C9 | ### Stable 测试网上的 USDT0 | 名称 | 值 | | ----------- | ------------------------------------------ | | wrapper | 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb | | composer | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D | | OFT | 0x779Ded0c9e1022225f8E0630b35a9b54bE713736 | | USDT0 impl | 0x3f9E27457ac494fC729beB50e6af04Ec34e3828E | | USDT0 proxy | 0x78Cf24370174180738C5B8E352B6D14c83a6c9A9 | ### Sepolia OFT 合约和 USDT0 合约(供参考) | 名称 | 值 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sepolia OFT | [https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126](https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126) | | Sepolia USDT | [https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract](https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) | ## 测试网信息 访问 Stable 测试网所需了解的一切信息。 ### 网络概览 | 配置项 | 值 | | ------------- | -------------- | | **网络名称** | Stable Testnet | | **Chain ID** | `2201` | | **Gas Token** | USDT0 | | **Gov Token** | STABLE | | **出块时间** | \~0.7 秒 | ### 区块浏览器 | 浏览器 | URL | | -------------- | ---------------------------------------------------------------- | | **Stablescan** | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) | ### RPC 端点 #### 主要端点 | 类型 | 端点 | 用途 | | ---------------- | ---------------------------------------------------------------- | ------ | | **EVM JSON-RPC** | [https://rpc.testnet.stable.xyz](https://rpc.testnet.stable.xyz) | EVM 交易 | | **WebSocket** | wss\://rpc.testnet.stable.xyz | 实时更新 | :::note 公共 RPC 端点的速率限制为**每个 IP 每 10 秒 1,000 次请求**。超过限制的请求将返回 `HTTP 429`。如需更高吞吐量,请使用[第三方 RPC 提供商](/cn/reference/rpc-providers)。 ::: ### 链信息 | 参数 | EVM | | ------------- | ------- | | **Chain ID** | `2201` | | **地址格式** | `0x...` | | **Gas Token** | `USDT0` | | **小数位数** | 18 | ### 水龙头与工具 | 工具 | URL | 描述 | | ------- | ------------------------------------------------------ | ------ | | **水龙头** | [https://faucet.stable.xyz](https://faucet.stable.xyz) | 获取测试代币 | | **快照** | 参见[节点运营者指南](/cn/how-to/use-node-snapshots) | 链快照 | ## 版本历史 Stable 测试网的完整版本历史及相关文档。 ### 当前版本信息 * **当前版本**: `v1.4.0-rc0` * **下次升级**: `TBD` * **升级高度**: `TBD` * **预计时间**: `TBD` ### 版本历史 #### 当前及历史版本 | 版本 | Commit | 升级高度 | 二进制文件 | 状态 | | -------------- | ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | **v1.4.0-rc0** | `83b5efb` | 57,806,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.4.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.4.0-rc0-linux-arm64-testnet.tar.gz) | 当前 | | **v1.3.1-rc0** | `75bb546` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.1-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.1-rc0-linux-arm64-testnet.tar.gz) | | | **v1.3.0-rc1** | `25b5e47` | 53,265,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc1-linux-arm64-testnet.tar.gz) | | | **v1.3.0-rc0** | `864d54c` | 49,855,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-arm64-testnet.tar.gz) | | | **v1.2.2-rc0** | `8bd5d5e` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-arm64-testnet.tar.gz) | | | **v1.2.1-rc1** | `7ff9a8a` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-arm64-testnet.tar.gz) | | | **v1.2.0-rc1** | `263c033` | 41,306,450 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz) | | | **v1.2.0** | `ee8ca35` | 40,392,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-arm64-testnet.tar.gz) | | | **v1.1.2** | `3d83aa3` | 34,649,300 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-arm64-testnet.tar.gz) | | | **v1.1.1** | `8becd6b` | 33,152,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-arm64-testnet.tar.gz) | | | **v1.1.0** | `17ceaa7` | 32,309,700 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-arm64-testnet.tar.gz) | | | **v0.8.1** | `1eb65d5` | 30,770,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-arm64-testnet.tar.gz) | | | **v0.8.0** | `e55efb6` | 29,410,999 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-arm64-testnet.tar.gz) | Bank 预编译增强 | | **v0.7.2** | `3c53e14` | 27,258,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-arm64-testnet.tar.gz) | StableBFT 集成 | | **v0.6.0** | `5cc1ad6` | 19,587,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.6.0-linux-amd64-testnet.tar.gz) | 小幅修复 | | **v0.5.0** | `919281d` | 18,719,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.5.0-linux-amd64-testnet.tar.gz) | 小幅修复 | | **v0.4.0** | `c6240c0` | 18,666,150 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.4.0-linux-amd64-testnet.tar.gz) | Stable SDK v0.53.4 | | **v0.3.0** | `a4f5ac5` | 9,166,131 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.3.0-linux-amd64-testnet.tar.gz) | EVM 价值转移允许列表 | | **v0.2.1** | `53e6e073` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.1-linux-amd64-testnet.tar.gz) | 非破坏性更新 | | **v0.2.0** | `8bdd771` | 8,956,584 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.0-linux-amd64-testnet.tar.gz) | 功能更新 | | **v0.1.0** | `10dfg542` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.1.0-linux-amd64-testnet.tar.gz) | 创世区块 (2025-04-07) | ### 相关文档 * [升级指南](/cn/how-to/upgrade-node) - 分步升级流程 * [测试网信息](/cn/reference/testnet-information) - 当前网络详情 ## 代币经济学 Stable 是一个高性能的第一层区块链,专为稳定币结算、企业级支付和以 USDT 为中心的基础设施而优化。 本代币经济学页面概述了 STABLE 代币的供应、分配和经济设计。 *** ### 概览 | 项目 | 详情 | | :------- | :-------------------- | | **代币符号** | STABLE | | **总供应量** | 100,000,000,000 代币 | | **标准** | ERC-20(Stable 主网 EVM) | | **小数位** | 18 | STABLE 是 Stable 主网和生态系统的治理代币,旨在支持验证者、开发者和用户之间的长期经济协调。 *** ### 代币分配 **总供应量:** 100,000,000,000 STABLE 代币 | 类别 | 分配 | STABLE 数量 | | :---------- | :--- | :-------------- | | **投资者与顾问** | 25% | 25,000,000,000 | | **团队** | 25% | 25,000,000,000 | | **生态系统与社区** | 40% | 40,000,000,000 | | **创世分配** | 10% | 10,000,000,000 | | **总计** | 100% | 100,000,000,000 | *** ### 发行模型与供应时间表 * 总供应量固定为 100,000,000,000 STABLE 代币。 * 仅有一部分供应量在 Stable 主网启动时进入流通。 * 团队和投资者与顾问分配需遵守 1 年锁定期的 4 年线性解锁模型,以确保长期承诺。 *** ### 分配详情 #### 创世分配 - 总代币供应量的 10% 旨在引导使用、为市场提供流动性、进行空投活动、奖励早期支持者以及与交易所和生态系统合作伙伴开展活动。 **解锁时间表** * Stable 主网启动时 100% 解锁 #### 生态系统与社区 - 总代币供应量的 40% 支持长期生态系统和社区增长: * 支持 Stable 软件和生态系统的开发 * 开发者资助 * 用户引导激励 * 支付合作伙伴集成 * 链上活动奖励 * 黑客松、大使计划 * 基础设施资助 **解锁时间表** * **初始解锁:** 在 Stable 主网启动时解锁总供应量的 8%,用于为战略启动合作伙伴提供激励、满足流动性需求以及实施早期生态系统增长活动。 * **总解锁期:** 此后对总供应量的 32% 进行 3 年线性解锁 #### 团队 - 总代币供应量的 25% * 分配给创始团队成员、工程师、研究人员和贡献者 * 旨在确保团队与 Stable 生态系统之间的长期协调。 **解锁时间表** * **1 年锁定期:** 前 12 个月不解锁任何代币 * **总解锁期:** 从 Stable 主网启动起 48 个月线性解锁 #### 投资者与顾问 - 总代币供应量的 25% 分配用于募资轮次和顾问支持。 **解锁时间表** * **1 年锁定期:** 前 12 个月不解锁任何代币 * **总解锁期:** 从 Stable 主网启动起 48 个月线性解锁 *** ### 发行图表 STABLE 代币发行图表 STABLE 代币发行图表 *** ### 经济设计原则 Stable 的代币经济学围绕三个基本目标设计: #### 1. 为支付优化的第一层提供动力 STABLE 代币激励高吞吐量、低延迟的基础设施,支持亚秒级区块确认和企业级结算保证。 #### 2. 支持可持续的生态系统增长 总代币供应量的 40% 用于长期增长,专注于关键开发和增长领域 * 开发者资助 * 合作伙伴集成 * 新生态系统应用 #### 3. 通过解锁机制协调长期贡献者 团队分配采用 1 年锁定期的 4 年线性解锁模型,确保长期协调和对网络开发的持续贡献。 *** ### STABLE 代币的实用性 STABLE 代币是 Stable 主网上的 ERC-20 治理代币。可用于: * 选举验证者 * 对协议升级进行投票 * 处理治理提案 * 作为从验证者接收 Gas 费用分配的凭证 在 Stable 网络上,所有交易都使用 USDT0 作为原生 Gas 代币。这些 USDT0 Gas 费用被收集到由智能合约管理的金库中。当代币持有者将其 STABLE 代币质押给验证者时,验证者可以选择将金库中的 Gas 费用按比例分配给质押者。 ## 钱包 ### 钱包概览表 | **提供商** | **类别** | **安全机制** | **文档 / 入门指南** | **说明** | | :----------------------------------------------------------------- | :-------------- | :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | | Stable Pay | 用户钱包 | 基于 TSS-MPC 的自托管 | [https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain](https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain) | 构建于 Stable 之上的 USDT 原生支付钱包;针对即时转账进行了优化 | | [Wallet Development Kit by Tether (WDK)](https://wallet.tether.io) | 钱包 SDK | 自托管 | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether 的开源 SDK,用于跨多链构建自托管钱包 | | [Binance Wallet](https://www.binance.com/en/web3wallet) | 用户钱包 | 基于 MPC 的自托管 / 半托管钱包 | [https://developers.binance.com/docs/binance-spot-api-docs/README](https://developers.binance.com/docs/binance-spot-api-docs/README) | 多链钱包,支持 Stable USDT | | [Reown](https://reown.com/)(前身为 WalletConnect / WalletKit) | 连接性 / 钱包基础设施 | 协议级签名与安全中继 | [https://docs.reown.com/appkit/overview](https://docs.reown.com/appkit/overview) | 支持 600+ 钱包、多链、基于 SDK 的集成,适合嵌入式钱包流程 | | [Bitget Wallet](https://web3.bitget.com/en) | 用户钱包 | 非托管钱包(私钥由用户管理) | [https://web3.bitget.com/en/docs/](https://web3.bitget.com/en/docs/) | 内置 dApp 浏览器;支持多资产与多链 | | [Gate Wallet](https://www.gate.com/)(Gate Onchain) | 用户钱包 | 与交易所关联的钱包 | [https://www.gate.com/](https://www.gate.com/) | 与交易所关联的钱包;适合 CEX ↔ 钱包流程 | | [OKX Wallet](https://web3.okx.com/)(OKX Onchain) | 用户钱包 | 非托管 / 用于恢复的 MPC | [https://www.okx.com/earn/onchain-earn](https://www.okx.com/earn/onchain-earn) | 集成交易所的多链钱包 | | [Anchorage](https://www.anchorage.com/) | 托管 / 机构钱包 | 银行级合规托管(联邦特许银行) | [https://www.anchorage.com/who-we-serve](https://www.anchorage.com/who-we-serve) | 面向稳定资产的机构级托管 | | [Dynamic](https://www.dynamic.xyz/) | 嵌入式 / 应用内钱包基础设施 | 通过 SDK 或后端的托管密钥 / 托管基础设施 | [https://www.dynamic.xyz/docs/introduction/welcome](https://www.dynamic.xyz/docs/introduction/welcome) | 使应用无需外部钱包即可嵌入钱包流程 | | [Alchemy](https://www.alchemy.com/) | 智能钱包与账户抽象基础设施 | Bundler + Paymaster 基础设施(ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | 驱动 AA 钱包;支持代付 gas、智能账户 | | [Atomic Wallet](https://atomicwallet.io/) | 用户钱包 | 非托管(密钥存储在用户设备上) | [https://atomicwallet.io/assets-status](https://atomicwallet.io/assets-status) | 支持多资产的移动端、桌面端和浏览器扩展钱包;通过内置的 [Simplex](/cn/reference/ramps#simplex-by-nuvei) 法币入口在 Stable 上购买 USDT | ### 类别指南 * **用户钱包:** 这些是面向消费者的传统钱包,例如移动应用、浏览器扩展或与交易所关联的钱包。它们允许用户持有 USDT、进行转账、连接 dApp,并直接与 Stable 交互。 * **钱包 SDK:** 一种软件开发工具包,为开发者提供预构建的工具、API 和基础设施,可将钱包创建、密钥管理、交易签名和区块链交互直接集成到其应用中。 * **托管 / 机构钱包:** 为机构提供受监管的企业级资产托管的平台。这些方案侧重于合规、治理管控、安全的密钥管理和资金运营,而非面向终端用户的流程。 * **嵌入式 / 应用内钱包:** 通过 SDK 或后端系统在应用内部生成的钱包。它们使主流用户无需安装或了解外部加密钱包即可实现无缝上手。 * **智能钱包 / 账户抽象:** 支持自定义逻辑的可编程钱包,例如免 gas 交易、批量操作或自动执行。它们通过开发者定义的行为扩展了基础钱包功能。 * **MPC 钱包提供商:** 使用多方计算(MPC)将私钥控制权分布在多方或多设备之间的密钥管理系统。适合需要高安全性托管而无需传统助记词的应用或企业。 * **连接性提供商:** 诸如 WalletConnect(Reown)之类的协议,用于连接钱包与 dApp。它们本身不存储资产或充当钱包;而是提供用于交易签名和交互的安全通信通道。 ### 1. 用户钱包 这些是由主要的全球性交易所提供的终端用户钱包。它们允许用户持有 USDT、转账资金,并连接到 Stable 上的应用。 #### Stable Pay 一款构建于 Stable 之上的非托管支付钱包,专为快速、稳定币原生的交易而设计。Stable Pay 提供即时 USDT 支付、可预测的费用,以及为日常转账和商业活动优化的简单用户体验。 **功能** * 面向 Stable 的非托管钱包 * 即时 USDT 支付 * 可预测且一致的交易成本 * 直接构建于 Stable 的 USDT 原生结算层之上 * 为支付与商业活动设计的消费者友好型界面 #### Binance Wallet 一款广泛使用的多链钱包,与按交易量计算全球最大的交易所集成。 **功能** * 支持 Stable USDT * 与 Binance 生态系统直接集成 * 提供移动端与扩展钱包选项 #### Bitget Wallet 一款与 Bitget 交易所生态系统连接的多资产钱包,支持加密货币、股票和 ETF。 **功能** * 支持 Stable USDT * 内置 dApp 浏览器 * 与 Bitget 交易账户无缝集成 #### Gate Wallet(Gate Onchain) 一款由全球最大的现货交易所之一支持的钱包产品。 **功能** * 支持 Stable USDT * 在 Gate 交易所与钱包之间轻松转账 * 支持 dApp 与网页应用连接 #### OKX Wallet(OKX Onchain) 一款功能强大、全球通用的多链钱包。 **功能** * 支持 Stable USDT * 深度集成 OKX 生态系统 * 提供网页端、移动端与扩展钱包选项 #### Atomic Wallet 一款非托管的多资产钱包,可用于桌面端、移动端及浏览器扩展。Atomic Wallet 集成了 [Simplex by Nuvei](/cn/reference/ramps#simplex-by-nuvei) 法币入口,因此用户可以在应用内直接使用银行卡、Apple Pay、Google Pay 或银行转账在 Stable 上购买 USDT。 **功能** * 在 Stable 上持有、发送和接收 USDT * 私钥本地存储在用户设备上 * 通过 Simplex 在应用内购买 Stable 上的 USDT * 支持 Windows、macOS、Linux、iOS、Android 和 Chrome **入门**:从 [atomicwallet.io](https://atomicwallet.io/downloads) 下载 Atomic Wallet,将 Stable 添加为网络,然后使用应用内的购买流程通过 Simplex 购买 Stable 上的 USDT。 ### 2. 钱包 SDK #### Development Kit by Tether (WDK) 来自 Tether 的开源 SDK,用于在任意平台和区块链上构建自托管钱包。 **功能** * 多链支持:Bitcoin、Ethereum、TON、TRON、Solana、Spark 等 * 代理型钱包:原生支持 AI 代理钱包及 Stable 上的 x402 支付 * DeFi 集成:插件式支持兑换、跨链桥和借贷协议 * 可扩展设计:可为新区块链或协议添加自定义模块 **入门**:安装 [`@tetherto/wdk`](https://www.npmjs.com/package/@tetherto/wdk) 和 [`@tetherto/wdk-wallet-evm`](https://www.npmjs.com/package/@tetherto/wdk-wallet-evm),然后按照 [WDK 文档](https://docs.wallet.tether.io)将 Stable 配置为目标链。 ### 3. 托管与机构钱包 #### Anchorage 一家联邦特许的国家银行,为数字资产提供机构级托管服务。 **功能** * 为 Stable USDT 提供安全托管 * 完全合规并接受监管 * 企业级密钥管理与访问控制 ### 4. 嵌入式 / 应用内钱包 通过 SDK 直接嵌入应用的钱包,可实现无缝的用户上手和支付流程。 #### Dynamic 企业级钱包基础设施,服务于数千款应用和超过 4000 万用户。 **功能** * 钱包创建与身份验证 * 嵌入式钱包流程 * 为应用和金融科技公司提供用户上手 **入门**:按照 [Dynamic SDK 设置文档](https://docs.dynamic.xyz/introduction/welcome)安装 SDK,并在你的应用中将 Stable 配置为受支持的网络。 #### Reown(前身为 WalletConnect) 一种广泛采用的标准,用于将钱包连接到应用。 **功能** * 安全的钱包到 dApp 连接 * 支持移动端、桌面端和扩展钱包 * 广泛的生态系统兼容性 **用于钱包上手的 Reown SDK** Stable 支持与 **Reown SDK** 集成,帮助开发者为用户提供无缝的钱包与上手体验。 Reown 提供一款开源的一体化 SDK,作为通往 WalletConnect Network 的官方网关。它使你的应用能够实现流畅的钱包连接、交易、登录、嵌入式钱包(邮箱和社交登录)、链上支付、应用内兑换等功能。 **入门** * 访问 Reown 文档:[https://docs.reown.com/overview](https://docs.reown.com/overview) ### 5. 智能钱包与账户抽象 支持可编程钱包、免 gas 交易、消费规则和高级用户体验的基础设施。 #### 概览表 | **提供商** | **类别** | **安全机制** | **文档 / 入门指南** | **说明** | | :------------------------------------------ | :------------------------- | :--------------------------------- | :------------------------------------------------------------------------------------- | :-------------------- | | [**Holdstation**](https://holdstation.com/) | 智能钱包(AA) | 智能合约钱包 + 生物识别认证 | [https://docs.holdstation.com/holdstation/](https://docs.holdstation.com/holdstation/) | 免 gas 流程,DeFi 原生钱包 | | [**Daimo**](https://pay.daimo.com/) | AA 支付钱包 | 智能账户,无助记词 | [https://paydocs.daimo.com/](https://paydocs.daimo.com/) | 一键支付,稳定币优先 | | [**Alchemy**](https://alchemy.com) | AA 基础设施(Bundler/Paymaster) | Bundler + paymaster 基础设施(ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | 使 AA 钱包能够在 Stable 上构建 | #### Alchemy Alchemy 提供部署智能账户、代付 gas 以及构建消费者级钱包所需的核心 AA 基础设施和 API。 **功能** * 智能账户 SDK 与 API * 为免 gas 操作提供 Paymaster 支持 * Gas 抽象工具 * 面向智能钱包开发者的可扩展基础设施 **入门**:使用 [Alchemy 智能账户 SDK](https://docs.alchemy.com) 部署 ERC-4337 智能账户,并配置 paymaster 以在 Stable 上代付 gas。 **文档**\ [https://docs.alchemy.com](https://docs.alchemy.com) #### Holdstation 一款提供账户抽象和生物识别安全交互的智能合约钱包。\ **功能** * 完整支持 AA 的智能钱包 * 免 gas 交易与代付费用 * 生物识别授权和会话密钥 * 集成的交易与 DeFi 执行层 **入门**:浏览 [Holdstation 开发者文档](https://docs.holdstation.com/holdstation/),将智能钱包流程和代付交易集成到你的应用中。 #### Daimo 一款消费者级的账户抽象钱包,专为即时稳定币消费和支付而设计。\ **功能** * 基于 AA 的用户体验与智能钱包执行 * 跨任意链的一键支付 * 无助记词;安全密钥恢复 * 适合支付应用和稳定币实用场景 **入门**:访问 [Daimo Pay 文档](https://paydocs.daimo.com/),为你在 Stable 上的应用添加一键稳定币支付流程。 ### Stable 网络设置 #### 将 Stable 添加到你的钱包 **网络参数:** * **Network Name:** Stable * **Chain ID:** 988 * **Currency:** USDT0 * **RPC URL:** [https://rpc.stable.xyz](https://rpc.stable.xyz) * **Block Explorer:** [https://stablescan.xyz](https://stablescan.xyz) #### 构建钱包集成 你可以通过以下方式添加 Stable 支持: * 通过 Stable RPC 启用签名和 gas 估算 * 支持 USDT 原生转账 * 为 dApp 集成 WalletConnect * 将 Stable 添加到链列表或元数据注册表中 ### 拥有集成 Stable 的钱包? 你可以通过 [bizdev@stable.xyz](mailto\:bizdev@stable.xyz) 联系团队,以便被列入本部分。 ## 使用 EIP-7702 实现账户抽象 本指南将逐步介绍如何将 EIP-7702 应用于 EOA,并利用委托实现三种模式:批量支付、支出限额和会话密钥。整个过程中,EOA 始终保持其地址和私钥不变。 :::note **概念:** 关于 EIP-7702 在 Stable 上能够实现的功能以及安全注意事项,请参阅 [EIP-7702](/cn/explanation/eip-7702)。 ::: ### 前提条件 * 了解 EOA 与智能合约账户的区别(EOA 默认没有代码)。 * 熟悉 EVM 交易类型([EIP-2718](https://eips.ethereum.org/EIPS/eip-2718))。 ### 概述 EIP-7702 引入了一种新的交易类型(`0x04`),该类型携带一个 `authorizationList`。每个授权都指定一个智能合约,EOA 将在该交易中执行该合约的代码。流程如下: 1. **选择或部署委托合约**:一个标准的 Solidity 合约,实现你希望 EOA 使用的逻辑。你可以使用已部署的合约,也可以部署自己的合约。请尽可能使用经过审计的合约。 2. **签署授权**:EOA 所有者签署一条消息,授权委托合约。 3. **提交 EIP-7702 交易**:该交易包含授权,EOA 在执行过程中运行委托合约的代码。 ### 用例:批量交易 下面的步骤将使用 `Multicall3` 作为委托合约来演示此流程。`Multicall3` 是一个广泛部署的实用合约,可将多个调用聚合到单个交易中。通过将 `Multicall3` 指定为 EIP-7702 委托,EOA 可以将任意合约交互(代币转账、授权、合约读取或任意组合)批量打包到一个原子交易中。批量支付便是其中一个示例:与其为一次工资发放发送十笔单独的交易,EOA 可以一次性执行所有交易。 #### 步骤 1:使用 Multicall3 作为委托合约 `Multicall3` 在 Stable 上部署于 `0xcA11bde05977b3631167028862bE2a173976CA11`。由于它已经部署且被广泛使用,你无需部署自己的委托合约。签署 EIP-7702 授权将授予委托合约对你的 EOA 的完整执行权限。 ```solidity // Multicall3 interface (relevant functions only) interface IMulticall3 { struct Call3 { address target; bool allowFailure; bytes callData; } struct Result { bool success; bytes returnData; } /// @notice Aggregate calls, allowing each to succeed or fail independently function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData); } ``` #### 步骤 2:签署授权 EOA 所有者签署一个指定委托合约的授权。该授权包含在 EIP-7702 交易中。 ```javascript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_TESTNET_CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const DELEGATE_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); ``` ```javascript // signAuthorization.ts import { ethers } from "ethers"; import { DELEGATE_ADDRESS, STABLE_TESTNET_CHAIN_ID, provider, wallet } from "./config"; export async function signAuthorization() { const authorization = { chainId: STABLE_TESTNET_CHAIN_ID, address: DELEGATE_ADDRESS, nonce: await provider.getTransactionCount(wallet.address), }; return wallet.signAuthorization(authorization); } ``` #### 步骤 3:提交 EIP-7702 交易 将授权与对 `Multicall3.aggregate3` 的调用结合起来。本示例在单个交易中批量执行三笔 USDT0 转账。 ```javascript import { ethers } from "ethers"; import { wallet, USDT0_ADDRESS } from "./config"; import { signAuthorization } from "./signAuthorization"; const usdt0Interface = new ethers.Interface([ "function transfer(address to, uint256 amount)", ]); const batchInterface = new ethers.Interface([ "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) returns ((bool success, bytes returnData)[])", ]); async function main() { const recipients = [ { to: "0xAlice...", amount: ethers.parseUnits("100", 6) }, { to: "0xBob...", amount: ethers.parseUnits("200", 6) }, { to: "0xCarol...", amount: ethers.parseUnits("150", 6) }, ]; const batchData = batchInterface.encodeFunctionData("aggregate3", [ recipients.map(({ to, amount }) => ({ target: USDT0_ADDRESS, allowFailure: false, callData: usdt0Interface.encodeFunctionData("transfer", [to, amount]), })), ]); const signedAuth = await signAuthorization(); const tx = await wallet.sendTransaction({ type: 4, // EIP-7702 transaction type to: wallet.address, // call is directed at the EOA itself data: batchData, // aggregate3 call to execute authorizationList: [signedAuth], maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Batch transactions executed in tx:", receipt.hash); } ``` ```text Batch transactions executed in tx: 0x... ``` EOA 通过 `Multicall3.aggregate3` 在单个原子交易中执行所有三个调用。委托会一直持续,直到被显式更改或清除。虽然本示例展示的是批量支付,但同样的模式适用于任意组合的合约调用。 ### 用例:支出限额 委托合约可以在不进行账户迁移的情况下,对 EOA 强制执行每笔交易或每日的限额。 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title SpendingLimitExecutor /// @notice Delegate contract that enforces daily spending caps contract SpendingLimitExecutor { mapping(address => uint256) public dailyLimit; mapping(address => uint256) public spentToday; mapping(address => uint256) public lastResetDay; function setDailyLimit(uint256 limit) external { dailyLimit[msg.sender] = limit; } function executeWithLimit( address target, uint256 value, bytes calldata data ) external payable { uint256 today = block.timestamp / 1 days; if (today > lastResetDay[msg.sender]) { spentToday[msg.sender] = 0; lastResetDay[msg.sender] = today; } spentToday[msg.sender] += value; require( spentToday[msg.sender] <= dailyLimit[msg.sender], "daily limit exceeded" ); (bool success,) = target.call{value: value}(data); require(success, "call failed"); } } ``` ### 用例:会话密钥 会话密钥允许 dApp 在限定的权限范围内代表 EOA 执行交易:一个时间窗口和一组允许的目标合约。这对于那些频繁的链上交互否则需要反复钱包批准的 dApp 非常有用。 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title SessionKeyExecutor /// @notice Delegate contract that grants scoped, time-limited access to a session key contract SessionKeyExecutor { struct Session { address key; uint256 validUntil; uint256 spendingLimit; uint256 spent; } mapping(address => Session) public sessions; mapping(address => mapping(address => bool)) public allowedTargets; /// @notice Register a session key with scoped permissions function startSession( address key, uint256 validUntil, uint256 spendingLimit, address[] calldata targets ) external { sessions[msg.sender] = Session({ key: key, validUntil: validUntil, spendingLimit: spendingLimit, spent: 0 }); for (uint256 i = 0; i < targets.length; i++) { allowedTargets[msg.sender][targets[i]] = true; } } /// @notice Execute a call using the session key function executeAsSessionKey( address owner, address target, uint256 value, bytes calldata data ) external { Session storage session = sessions[owner]; require(msg.sender == session.key, "not session key"); require(block.timestamp <= session.validUntil, "session expired"); require(allowedTargets[owner][target], "target not allowed"); uint256 beforeBalance = owner.balance; (bool success,) = target.call{value: value}(data); require(success, "call failed"); session.spent += owner.balance - beforeBalance; require(session.spent <= session.spendingLimit, "budget exceeded"); } /// @notice Revoke the active session function revokeSession() external { delete sessions[msg.sender]; } } ``` ### 重要注意事项 * **持久委托**:委托会一直持续,直到 EOA 显式更改或清除它。它不限于单个交易。 * **Gas 成本**:由于授权处理,EIP-7702 交易的基础 Gas 略高,但在委托批量处理多个调用时可以抵消这部分成本。 * **使用经过审计的委托**:恶意的委托合约可能会耗尽 EOA 的资产。只委托给经过审计的合约。 ### 关键要点 * EIP-7702 允许 EOA 执行智能合约逻辑,而无需迁移到新的账户类型。 * 在 Stable 上,EIP-7702 可在现有 EOA 上实现批量支付、支出限额和限定范围的会话密钥。 * 委托会一直持续,直到被显式更改。请始终使用经过审计的委托合约。 ### 下一步推荐 * [**订阅与收款**](/cn/how-to/subscribe-and-collect) — 使用 SubscriptionManager 将 EIP-7702 应用于周期性订阅支付。 * [**EIP-7702 概念**](/cn/explanation/eip-7702) — 在上线之前理解委托模型。 * [**EIP-7702 参考**](/cn/reference/eip-7702-api) — 查阅 `0x04` 交易格式和授权字段。 ## 在 Stable 上构建 MPP 端点 本指南将逐步介绍如何为 Stable 上的 USDT0 编写自定义 [MPP](/cn/explanation/mpp) 支付方法,并提供一个受 MPP 保护的端点。买方签署一个 [ERC-3009](/cn/explanation/erc-3009) `transferWithAuthorization`,服务器通过 `mppx` 的 `verify()` 钩子对其进行验证,结算则在你控制的单独步骤中完成。 :::note **概念:** 关于什么是 MPP 以及它与 x402 的关系,请参阅 [机器支付协议(MPP)](/cn/explanation/mpp)。关于 x402 的等价实现,请参阅 [构建按次调用付费 API](/cn/how-to/build-pay-per-call)。 ::: :::note 该示例使用 Stable 主网。测试时请使用小额金额。 ::: ### 你将构建的内容 一个 HTTP 端点,它返回 `402 Payment Required` 以及一个 MPP `WWW-Authenticate` 挑战,接受 `Authorization` 标头中已签名的凭据,对其进行验证,在 USDT0 上结算 `transferWithAuthorization`,并返回带有 `Payment-Receipt` 标头的响应。 ```text step 1. Client: GET /weather (no Authorization header) Server: 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[...usdt0-stable charge for $0.001...]" step 2. Client signs an ERC-3009 authorization with their viem account step 3. Client: GET /weather + Authorization header containing the serialized credential Server: verify() validates the EIP-712 signature Server: settle() submits transferWithAuthorization on Stable (~700ms block confirmation) Server: 200 OK { weather: "sunny" } Payment-Receipt: reference="0x8f3a...", status="success" step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a... ``` ### 前提条件 * 在 Stable 上有一个已注资的 USDT0 钱包。请参阅 [使用水龙头](/cn/how-to/use-faucet) 或 [转移 USDT0](/cn/tutorial/send-usdt0)。 * 安装了 `mppx`、`viem` 和 `zod` 的 Node 20+。 * 在 Stable 上有一个卖方账户(一个 EOA)。在默认结算路径下,卖方以 USDT0 支付 gas;[Gas Waiver](#alternative-settle-through-the-gas-waiver) 一节展示了零 gas 的变体。 ```bash npm install mppx viem zod express ``` ### 1. 定义共享 schema `Method.from()` 声明意图以及请求(Challenge)和凭据载荷的 schema。客户端和服务器都导入此定义。 ```typescript // src/method.ts import { Method } from "mppx"; import { z } from "zod"; import { parseUnits } from "viem"; export const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; export const CHAIN_ID = 988; // Request: the Challenge payload the server sends to the client. const zRequest = z.pipe( z.object({ chainId: z.literal(CHAIN_ID), asset: z.literal(USDT0_STABLE), amount: z.string(), // human-readable, e.g. "0.001" decimals: z.literal(6), payTo: z.string().regex(/^0x[a-fA-F0-9]{40}$/), validAfter: z.number().int().nonnegative(), validBefore: z.number().int().positive(), nonce: z.string().regex(/^0x[a-fA-F0-9]{64}$/), }), z.transform(({ amount, decimals, ...rest }) => ({ ...rest, amount: parseUnits(amount, decimals).toString(), // atomic units })), ); // Credential payload: what the client returns after signing. const zPayload = z.object({ from: z.string().regex(/^0x[a-fA-F0-9]{40}$/), signature: z.string().regex(/^0x[a-fA-F0-9]{130}$/), // 65-byte hex }); export const usdt0Stable = Method.from({ intent: "charge", name: "usdt0-stable", schema: { request: zRequest, credential: { payload: zPayload } }, }); // EIP-712 domain + type, used by both client and server. export const EIP712_DOMAIN = { name: "USDT0", version: "1", chainId: CHAIN_ID, verifyingContract: USDT0_STABLE, } as const; export const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], } as const; ``` ```text usdt0Stable.name === "usdt0-stable" usdt0Stable.intent === "charge" ``` ### 2. 服务器:验证凭据 `Method.toServer` 将 `verify()` 接入 `mppx`。该函数接收反序列化后的凭据(挑战 + 载荷),并且必须在证明无效时抛出异常,或者返回一个 `Receipt`。 ```typescript // src/server-method.ts import { Method, Receipt } from "mppx"; import { verifyTypedData } from "viem"; import { usdt0Stable, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPES, } from "./method"; export const usdt0StableServer = Method.toServer(usdt0Stable, { async verify({ credential }) { const { request } = credential.challenge; const { from, signature } = credential.payload; const valid = await verifyTypedData({ address: from as `0x${string}`, domain: EIP712_DOMAIN, types: TRANSFER_WITH_AUTHORIZATION_TYPES, primaryType: "TransferWithAuthorization", message: { from: from as `0x${string}`, to: request.payTo as `0x${string}`, value: BigInt(request.amount), validAfter: BigInt(request.validAfter), validBefore: BigInt(request.validBefore), nonce: request.nonce as `0x${string}`, }, signature: signature as `0x${string}`, }); if (!valid) throw new Error("Invalid ERC-3009 signature"); // The Receipt's reference is filled in with the tx hash after settle(). return Receipt.from({ method: usdt0Stable.name, reference: "pending", status: "success", timestamp: new Date().toISOString(), }); }, }); ``` ```text { method: "usdt0-stable", reference: "pending", status: "success", timestamp: "2026-06-01T12:34:56.000Z" } ``` :::warning `verify()` 检查签名,但不检查 nonce 唯一性或授权是否已被花费。链在提交时强制执行这两者:`transferWithAuthorization` 在 nonce 已被使用时会回退。结算步骤会将这些回退转化为服务器可以向客户端展现的错误。 ::: ### 3. 结算:提交 `transferWithAuthorization` 结算被有意地与 `verify()` 分离。在 `verify()` 返回后,你通过适合你运维模型的任意路径将授权提交到链上。以下是三个选项,按推荐顺序排列。 #### 默认:服务器直接提交 卖方的 EOA 将带有已签名授权的 `transferWithAuthorization` 提交到 USDT0。卖方以 USDT0(Stable 的原生 gas 代币)支付 gas,因此无需管理单独的 gas 代币余额。 ```typescript // src/settle.ts import { createWalletClient, http, parseSignature } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { stable } from "viem/chains"; import { USDT0_STABLE } from "./method"; const USDT0_ABI = [ { name: "transferWithAuthorization", type: "function", stateMutability: "nonpayable", inputs: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, { name: "v", type: "uint8" }, { name: "r", type: "bytes32" }, { name: "s", type: "bytes32" }, ], outputs: [], }, ] as const; const seller = privateKeyToAccount(process.env.SELLER_KEY as `0x${string}`); const wallet = createWalletClient({ account: seller, chain: stable, transport: http("https://rpc.stable.xyz"), }); export async function settleDirect(credential: { challenge: { request: any }; payload: { from: string; signature: string }; }): Promise<{ txHash: `0x${string}` }> { const { request } = credential.challenge; const { v, r, s } = parseSignature(credential.payload.signature as `0x${string}`); const txHash = await wallet.writeContract({ address: USDT0_STABLE, abi: USDT0_ABI, functionName: "transferWithAuthorization", args: [ credential.payload.from as `0x${string}`, request.payTo as `0x${string}`, BigInt(request.amount), BigInt(request.validAfter), BigInt(request.validBefore), request.nonce as `0x${string}`, Number(v), r as `0x${string}`, s as `0x${string}`, ], }); return { txHash }; } ``` ```text { txHash: "0x8f3a1b2c..." } ``` #### 替代方案:通过 Gas Waiver 结算 使用 Stable 的 [Gas Waiver](/cn/how-to/integrate-gas-waiver) 以 `gasPrice = 0` 提交内层交易。卖方仍然签署包装交易,但不支付 gas。需要一个 Waiver Server API 密钥。 ```typescript // src/settle-waiver.ts import { encodeFunctionData } from "viem"; import { USDT0_STABLE } from "./method"; import { USDT0_ABI } from "./settle"; const WAIVER_SERVER = "https://waiver.stable.xyz"; // mainnet endpoint export async function settleViaWaiver( credential: { challenge: { request: any }; payload: { from: string; signature: string } }, signedInnerTxHex: `0x${string}`, ): Promise<{ txHash: `0x${string}` }> { const res = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.WAIVER_API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex] }), }); const lines = (await res.text()).trim().split("\n"); const result = JSON.parse(lines[0]); if (!result.success) throw new Error(`Settle failed: ${result.error?.message}`); return { txHash: result.txHash }; } ``` ```text { txHash: "0x8f3a1b2c..." } ``` 关于如何在发布前构建已签名的内层交易(`gasPrice: 0`,已编码的 `transferWithAuthorization` 调用),请参阅 [Gas waiver 协议](/cn/reference/gas-waiver-api)。 #### 替代方案:交给 x402 facilitator 如果你已经运行了 x402 facilitator 集成([Semantic Pay](https://docs.semanticpay.io) 或 [Heurist](https://docs.heurist.ai/x402-products/facilitator)),你可以将其复用为结算目标。向 `/settle` POST 一个 `paymentPayload`;facilitator 会提交链上调用。 `paymentPayload` 的确切结构是 x402 中间件内部的,未在线路层面指定。最简单的路径是使用 facilitator 自己的 SDK 来构建载荷,或者坚持使用上面的直接提交路径。facilitator 不需要支持 MPP;它只看到 `transferWithAuthorization` 字段。 ### 4. 客户端:签署凭据 `Method.toClient` 将 `createCredential()` 接入 `mppx`。客户端读取 Challenge,使用代理的 viem 账户签署 EIP-712 授权,并序列化凭据。 ```typescript // src/client-method.ts import { Credential, Method } from "mppx"; import { hexToSignature, parseSignature } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { usdt0Stable, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPES, } from "./method"; export function createUsdt0StableClient(privateKey: `0x${string}`) { const account = privateKeyToAccount(privateKey); return Method.toClient(usdt0Stable, { async createCredential({ challenge }) { const { request } = challenge; const signature = await account.signTypedData({ domain: EIP712_DOMAIN, types: TRANSFER_WITH_AUTHORIZATION_TYPES, primaryType: "TransferWithAuthorization", message: { from: account.address, to: request.payTo as `0x${string}`, value: BigInt(request.amount), validAfter: BigInt(request.validAfter), validBefore: BigInt(request.validBefore), nonce: request.nonce as `0x${string}`, }, }); return Credential.serialize({ challenge, payload: { from: account.address, signature }, }); }, }); } ``` ```text "eyJjaGFsbGVuZ2UiOnsi..." // base64-serialized credential, ~600 bytes ``` ### 5. 将服务器组装起来 使用 `mppx` 的 Express 中间件来发出 Challenge、解析传入的 `Authorization` 标头、运行 `verify()`、调用你的结算函数,并发出 `Payment-Receipt` 标头。 ```typescript // src/server.ts import express from "express"; import { Mppx } from "mppx/express"; import { randomBytes } from "node:crypto"; import { usdt0StableServer } from "./server-method"; import { settleDirect } from "./settle"; const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`; const PORT = Number(process.env.PORT ?? 4022); const mppx = Mppx.create({ secretKey: process.env.MPP_SECRET_KEY!, methods: [usdt0StableServer], onVerified: async ({ credential, receipt }) => { const { txHash } = await settleDirect(credential); return { ...receipt, reference: txHash }; }, }); const app = express(); app.get( "/weather", mppx.charge({ amount: "0.001", method: "usdt0-stable", request: { chainId: 988, asset: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", decimals: 6, payTo: PAY_TO, validAfter: 0, validBefore: Math.floor(Date.now() / 1000) + 300, nonce: `0x${randomBytes(32).toString("hex")}`, }, })((_req, res) => { res.json({ weather: "sunny", temperature: 70 }); }), ); app.listen(PORT, () => { console.log(`MPP server listening on http://localhost:${PORT}`); }); ``` ```text MPP server listening on http://localhost:4022 ``` ### 6. 端到端运行整个流程 启动服务器,确认 Challenge,运行客户端,并确认结算。 :::warning 下一步将在 Stable 主网上结算一笔真实的 USDT0 付款。请使用专用钱包和小额金额。 ::: #### 确认 Challenge ```bash curl -i http://localhost:4022/weather ``` ```text HTTP/1.1 402 Payment Required WWW-Authenticate: Payment realm="...", challenges="[{\"method\":\"usdt0-stable\",\"request\":{...}}]" Content-Type: application/json {"error":"Payment required"} ``` #### 发送一个付费请求 ```typescript // src/client.ts import { Mppx } from "mppx/client"; import { createUsdt0StableClient } from "./client-method"; const client = Mppx.create({ methods: [createUsdt0StableClient(process.env.BUYER_KEY as `0x${string}`)], }); const res = await fetch("http://localhost:4022/weather", { // mppx wraps fetch with the 402 retry loop: ...client.fetchOptions(), }); console.log(res.status, await res.json()); console.log("Payment-Receipt:", res.headers.get("Payment-Receipt")); ``` ```bash npx tsx src/client.ts ``` ```text 200 { weather: "sunny", temperature: 70 } Payment-Receipt: reference="0x8f3a1b2c...", status="success", timestamp="2026-06-01T12:34:56.000Z" ``` #### 在 Stablescan 上验证 打开 `https://stablescan.xyz/tx/0x8f3a1b2c...`,确认 `transferWithAuthorization` 已结算到你的 `PAY_TO` 地址。 ### 你刚刚完成的内容 * 以 USDT0 付款,以美元计价,买方一侧无需管理 gas 代币余额。 * 在客户端到服务器的跳转上使用了 MPP 的 `WWW-Authenticate` / `Authorization` / `Payment-Receipt` 线路格式。 * 在同一个 HTTP 请求生命周期内(约 700 毫秒的出块时间)使用 `transferWithAuthorization` 在 Stable 上完成了结算。 ### 推荐的后续内容 * [**MPP 概念**](/cn/explanation/mpp) — 了解 MPP 如何与 x402 关联,以及其他意图是什么样的。 * [**MPP 会话**](/cn/explanation/mpp-sessions) — 当按请求结算成本过高时,使用链下凭证流式传输微支付。 * [**Facilitators**](/cn/reference/agentic-facilitators) — 使用 Semantic Pay 或 Heurist 作为结算目标,而不是直接提交。 ## 学习 P2P 支付 本指南将带你在 Stable 上构建一个 P2P 支付应用。该应用处理完整的支付生命周期:发送方直接转账 USDT0,接收方实时检测到入账支付,双方都可以查询各自的交易历史。无论是移动应用、网页结账,还是后端服务,任何钱包或支付界面的架构都与此相同。 无需中间件,无需中介。如需了解概念性概述,请参阅 [P2P 支付](/cn/reference/p2p-payments)。如果想跳过 ABI 相关工作、用几行代码实现可用的 `transfer`,请使用 [Stable SDK](/cn/explanation/sdk-overview)。 ### 你将构建的内容 构成一个最小支付应用的五个脚本: * `wallet.ts` — 创建或恢复钱包。 * `getBalance.ts` — 查询当前 USDT0 余额。 * `send.ts` — 向另一个地址发送 USDT0。 * `receive.ts` — 实时监听入账支付。 * `history.ts` — 查询某个地址过去的 Transfer 事件。 #### 演示 ```text step 1. Alice creates wallet → address: 0xAlice... step 2. Alice's balance: 0.01 USDT0 step 3. Alice sends 0.001 USDT0 to Bob tx: 0x8f3a...2d41 gas fee: 0.000021 USDT0 Alice balance: 0.008979 USDT0 step 4. Bob receives payment (real-time event) from: 0xAlice... amount: 0.001 USDT0 tx: 0x8f3a...2d41 ``` ### 前置条件 * Node.js 20 或更高版本。 * 一个拥有测试网 USDT0 的私钥(参见[快速开始](/cn/tutorial/quick-start)为钱包注资)。 ### 项目设置 ```bash mkdir stable-p2p && cd stable-p2p npm init -y && npm install ethers dotenv ``` ```text added 2 packages, audited 3 packages in 1s ``` 创建一个被所有脚本共享的 `config.ts`: ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const STABLE_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_WS = "wss://rpc.testnet.stable.xyz"; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_RPC); ``` ### 1. 创建或恢复钱包 钱包是从助记词派生出来的密钥对。为新用户生成一个钱包,并返回助记词以便他们备份。回访用户可以用相同的助记词恢复钱包。 ```typescript // wallet.ts import { ethers } from "ethers"; import { provider } from "./config"; /** Create a new wallet for a new user. */ export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // display to user for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address }; } if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase); } ``` ```bash npx tsx wallet.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` ### 2. 查询余额 USDT0 是 Stable 上的原生资产,因此余额查询与以太坊上的 ETH 完全相同。原生余额为 18 位小数,使用 `formatEther` 进行显示。 ```typescript // getBalance.ts import { ethers } from "ethers"; import { provider } from "./config"; export async function getBalance(address: string) { const balance = await provider.getBalance(address); return ethers.formatEther(balance); // 18 decimals } if (import.meta.url === `file://${process.argv[1]}`) { const address = process.argv[2]; const balance = await getBalance(address); console.log("Balance:", balance, "USDT0"); } ``` ```bash npx tsx getBalance.ts 0xAlice...1234 ``` ```text Balance: 0.01 USDT0 ``` ### 3. 发送支付 发送方直接签名并提交转账。在 Stable 上,USDT0 是原生资产,因此简单的价值转账是最便宜的方式(21,000 gas)。这与任何支付应用中的"发送"功能采用相同的代码路径。 ```typescript // send.ts import { ethers } from "ethers"; import { provider } from "./config"; export async function sendPayment( senderKey: string, recipient: string, amount: string // e.g. "0.001" for 0.001 USDT0 ) { const wallet = new ethers.Wallet(senderKey, provider); const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const tx = await wallet.sendTransaction({ to: recipient, value: ethers.parseEther(amount), maxFeePerGas: baseFee * 2n, maxPriorityFeePerGas: 0n, // always 0 on Stable }); console.log("Payment sent:", tx.hash); const receipt = await tx.wait(1); if (receipt!.status === 1) console.log("Payment settled"); return tx.hash; } if (import.meta.url === `file://${process.argv[1]}`) { const [, , recipient, amount] = process.argv; await sendPayment(process.env.PRIVATE_KEY!, recipient, amount); } ``` ```bash npx tsx send.ts 0xBob...5678 0.001 ``` ```text Payment sent: 0x8f3a...2d41 Payment settled ``` ### 4. 实时接收支付 接收方监听入账的 `Transfer` 事件。这相当于传统支付应用中的推送通知。在 Stable 上,单槽最终性意味着接收方几乎可以即时看到支付。 ```typescript // receive.ts import { ethers } from "ethers"; import { STABLE_WS, USDT0_ADDRESS } from "./config"; const wsProvider = new ethers.WebSocketProvider(STABLE_WS); const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], wsProvider ); export function watchIncomingPayments(address: string) { const filter = usdt0.filters.Transfer(null, address); usdt0.on(filter, (from: string, to: string, value: bigint, event: any) => { console.log("Payment received:"); console.log(" from: ", from); console.log(" amount:", ethers.formatUnits(value, 6), "USDT0"); console.log(" tx: ", event.log.transactionHash); }); console.log("Watching for incoming payments to", address); } if (import.meta.url === `file://${process.argv[1]}`) { watchIncomingPayments(process.argv[2]); } ``` ```bash npx tsx receive.ts 0xBob...5678 ``` ```text Watching for incoming payments to 0xBob...5678 Payment received: from: 0xAlice...1234 amount: 0.001 USDT0 tx: 0x8f3a...2d41 ``` :::note 原生转账(价值转账)同样会在 USDT0 ERC-20 合约上触发一个 `Transfer` 事件,因为在 Stable 上 USDT0 既是原生资产,也是一个 ERC-20 代币。单个事件监听器即可覆盖这两种转账方式。 ::: ### 5. 查询交易历史 查询过去的 `Transfer` 事件以构建交易历史视图,就像任何支付应用中的银行对账单或交易列表一样。 ```typescript // history.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], provider ); export async function getTransactionHistory(address: string, fromBlock?: number) { if (fromBlock === undefined) { const latest = await provider.getBlockNumber(); fromBlock = Math.max(0, latest - 10_000); } const [sentEvents, receivedEvents] = await Promise.all([ usdt0.queryFilter(usdt0.filters.Transfer(address, null), fromBlock), usdt0.queryFilter(usdt0.filters.Transfer(null, address), fromBlock), ]); return [ ...sentEvents.map((e: any) => ({ type: "sent" as const, counterparty: e.args[1], amount: ethers.formatUnits(e.args[2], 6), txHash: e.transactionHash, block: e.blockNumber, })), ...receivedEvents.map((e: any) => ({ type: "received" as const, counterparty: e.args[0], amount: ethers.formatUnits(e.args[2], 6), txHash: e.transactionHash, block: e.blockNumber, })), ].sort((a, b) => b.block - a.block); } if (import.meta.url === `file://${process.argv[1]}`) { const history = await getTransactionHistory(process.argv[2]); for (const tx of history) { console.log(`${tx.type} ${tx.amount} USDT0 ${tx.counterparty} ${tx.txHash}`); } } ``` ```bash npx tsx history.ts 0xAlice...1234 ``` ```text sent 0.001 USDT0 0xBob...5678 0x8f3a...2d41 received 0.01 USDT0 0xFaucet... 0x22b1...3f09 ``` :::warning 扫描大范围的区块(数百万个区块)可能会超时并超过 RPC 速率限制。在生产环境中,请使用 [Stablescan 的 Etherscan 兼容 API](https://stablescan.xyz) 进行分页历史查询——每一笔交易都已被索引。 ::: ### 推荐后续阅读 * [**订阅与收款**](/cn/how-to/subscribe-and-collect) — 基于拉取的周期性订阅,使用 EIP-7702 委托。 * [**使用发票付款**](/cn/how-to/pay-with-invoice) — 使用 ERC-3009 和确定性 nonce 结算发票。 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 参考基本的原生转账与 ERC-20 转账流程。 ## 构建按调用付费的 API 本指南介绍如何使用 x402 将 API 端点货币化。服务端添加支付处理逻辑,客户端按请求付费,结算在 HTTP 生命周期内完成。 :::note **概念:** 关于 x402 协议及其为何适合 Stable,请参阅 [x402](/cn/explanation/x402)。关于高层用例模型,请参阅 [按调用付费 API](/cn/reference/pay-per-call)。 ::: :::note Semantic 结算方目前仅在主网上运行。本指南中的示例使用 Stable 主网。测试时请使用小额金额。 ::: ### 你将构建的内容 一个付费 HTTP API,服务端以 `402 Payment Required` 响应,客户端按请求付费,结算方在 HTTP 生命周期内在链上结算 USDT0。 #### 演示 ```text step 1. Client: GET /weather (no payment) Server: 402 Payment Required PAYMENT-REQUIRED: { amount: "1000", asset: USDT0, network: eip155:988 } step 2. Client signs ERC-3009 authorization step 3. Client: GET /weather + PAYMENT-SIGNATURE header Server: forwards to facilitator → transferWithAuthorization settles on-chain (~700ms block confirmation) Server: 200 OK { weather: "sunny", temperature: 70 } PAYMENT-SETTLE-RESPONSE: { txHash: "0x8f3a...", paid: "0.001 USDT0" } step 4. Verify settlement on Stablescan https://stablescan.xyz/tx/0x8f3a... ``` ### 概览 **卖方(服务端):** ```typescript // --- Server --- app.use(paymentMiddleware({ "GET /weather": { price: { amount: "1000", asset: USDT0 }, payTo: sellerAddress, }, "POST /inference": { price: { amount: "50000", asset: USDT0 }, payTo: sellerAddress, }, }, resourceServer)); // Routes not listed in the config are not gated. ``` **买方(客户端):** ```typescript // --- Client --- account = new WalletAccountEvm(seedPhrase, { provider: RPC }); client = new x402Client(); fetchWithPayment = wrapFetchWithPayment(fetch, client); weatherResponse = fetchWithPayment("https://api.example.com/weather"); inferenceResponse = fetchWithPayment("https://api.example.com/inference", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: "Hello" }), }); // For each paid request: // 1. Initial request returns 402 with PAYMENT-REQUIRED header // 2. Client signs ERC-3009 authorization with wallet // 3. Client retries with PAYMENT-SIGNATURE header // 4. Facilitator settles on-chain, server returns the response ``` ### 卖方:设置付费端点 卖方添加 x402 中间件来定义哪些路由需要付费。当请求到达但没有携带支付时,中间件以 `402 Payment Required` 和支付条款响应。当存在有效的支付标头时,中间件将其转发给结算方,由结算方验证签名并在链上结算支付。卖方只需配置价格和收款地址;结算方负责验证和结算。 ```bash npm install express @x402/express @x402/evm @x402/core ``` #### 定价 每个路由以 USDT0 基本单位(6 位小数)指定支付金额、网络和收款地址。例如,`"1000"` 等于 `$0.001`,`"50000"` 等于 `$0.05`。 ```typescript price: { amount: "1000", // base units (6 decimals) asset: USDT0_STABLE, // USDT0 contract address extra: { name: "USDT0", version: "1", decimals: 6 }, // EIP-712 domain info } ``` `extra` 字段(`name`、`version`、`decimals`)由买方的客户端用于构建 EIP-712 签名,必须与链上 USDT0 合约相匹配。 #### 路由配置 路由使用 `METHOD /path` 格式映射。每个路由指定接受的支付方案、网络、价格和收款地址(`payTo`)。`description` 和 `mimeType` 字段帮助买方和 AI 代理发现该端点提供的内容。未在配置中列出的路由不受限制,行为与普通 Express 路由一致。 ```typescript // server.ts import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`; const FACILITATOR_URL = "https://x402.semanticpay.io/"; const STABLE_NETWORK = "eip155:988"; // Stable Mainnet CAIP-2 ID const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL }); const resourceServer = new x402ResourceServer(facilitatorClient) .register(STABLE_NETWORK, new ExactEvmScheme()); const app = express(); app.use( paymentMiddleware( { // Example 1: Configure a paid GET route "GET /weather": { accepts: [ { scheme: "exact", network: STABLE_NETWORK, price: { amount: "1000", // $0.001 asset: USDT0_STABLE, extra: { name: "USDT0", version: "1", decimals: 6 }, }, payTo: PAY_TO, }, ], description: "Weather data", mimeType: "application/json", }, // Example 2: Configure a paid POST route "POST /inference": { accepts: [ { scheme: "exact", network: STABLE_NETWORK, price: { amount: "50000", // $0.05 asset: USDT0_STABLE, extra: { name: "USDT0", version: "1", decimals: 6 }, }, payTo: PAY_TO, }, ], description: "AI inference endpoint", mimeType: "application/json", }, }, resourceServer, ), ); app.get("/weather", (req, res) => { res.json({ weather: "sunny", temperature: 70 }); }); app.post("/inference", (req, res) => { const { prompt } = req.body; res.json({ result: `Inference result for: ${prompt}` }); }); // Not listed in the config, so no payment required. app.get("/health", (req, res) => { res.json({ status: "ok", payTo: PAY_TO }); }); const PORT = process.env.PORT || 4021; app.listen(PORT, () => { console.log(`Server listening at http://localhost:${PORT}`); console.log(`GET /health - free`); console.log(`GET /weather - $0.001 per request`); console.log(`POST /inference - $0.05 per request`); }); ``` :::note x402 还为 Hono(`@x402/hono`)和 Next.js(`@x402/next`)提供了中间件。模式相同:创建结算方客户端、注册 EVM 方案并应用中间件。 ::: ### 买方:发起付费请求 买方无需经过手动支付流程即可访问付费端点。买方不支付 gas。结算方在链上结算,买方只支付支付要求中指定的确切金额。 ```bash npm install @x402/fetch @x402/evm @tetherto/wdk-wallet-evm ``` #### 创建钱包并检查余额 ```typescript // client.ts import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, { provider: "https://rpc.stable.xyz", }).getAccount(0); console.log("Buyer address:", account.address); // USDT0 uses 6 decimals. A balance of 1000000 equals 1.00 USDT0. const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const balance = await account.getTokenBalance(USDT0_STABLE); console.log("USDT0 balance:", Number(balance) / 1e6, "USDT0"); ``` #### 连接到 x402 并发起付费请求 `WalletAccountEvm` 满足 x402 所期望的签名者接口,因此可以直接注册为 x402 客户端的签名者。注册后,通过启用 x402 的客户端发送的请求会自动处理 402 支付流程。 ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; const client = new x402Client(); registerExactEvmScheme(client, { signer: account }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("http://localhost:4021/weather"); const data = await response.json(); console.log("Response:", data); ``` 在底层,`fetchWithPayment` 会拦截 402 响应,解析支付要求(金额、代币、网络、收款方),用 WDK 钱包签署一个 ERC-3009 `transferWithAuthorization`,并使用 `PAYMENT-SIGNATURE` 标头重试请求。 :::note 如果你更喜欢 Axios,可使用 `@x402/axios` 配合 `wrapAxiosWithPayment` 来获得相同的自动支付处理。 ::: ### 测试支付流程 启动服务端并验证付费路由和免费路由。 :::warning 此测试流程在 Stable 主网上运行。每次成功的付费请求都会通过托管的结算方结算一笔真实的 USDT0 支付。请仅使用专用钱包和小额金额。 ::: #### 1. 确认 402 响应 ```bash curl -i http://localhost:4021/weather ``` 响应应为 `402 Payment Required`,并带有包含价格、资产和网络的 `PAYMENT-REQUIRED` 标头。 #### 2. 运行客户端 ```bash npx tsx client.ts ``` 客户端处理完整流程:接收 402、签署授权、携带支付重试并打印响应。 #### 3. 读取收据 在付费请求成功后,买方可以从服务端响应中读取 `PAYMENT-SETTLE-RESPONSE` 标头并解析结算收据。 ```typescript // (continued) client.ts import { x402HTTPClient } from "@x402/fetch"; const httpClient = new x402HTTPClient(client); const receipt = httpClient.getPaymentSettleResponse( (name) => response.headers.get(name), ); console.log("Payment receipt:", JSON.stringify(receipt, null, 2)); ``` ### 在没有实时结算方的情况下测试 由于 Semantic 结算方仅支持主网,目前你无法将服务端指向测试网结算方。要在不结算真实支付的情况下迭代服务端逻辑、路由处理程序和中间件行为,可以对结算方客户端进行打桩(stub)。 ```typescript // server.test.ts import { x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; // Stub facilitator: accepts any signature, returns a fake settlement. const stubFacilitatorClient = { verify: async () => ({ isValid: true, payer: "0xMockPayer" }), settle: async () => ({ success: true, txHash: "0xMOCK000000000000000000000000000000000000000000000000000000000001", networkId: "eip155:988", }), }; export const testResourceServer = new x402ResourceServer(stubFacilitatorClient as any) .register("eip155:988", new ExactEvmScheme()); ``` 针对该桩运行单元测试以验证: * 402 响应包含正确的 `PAYMENT-REQUIRED` 负载。 * 带有有效 `PAYMENT-SIGNATURE` 标头的请求能够到达处理程序。 * 缺失或格式错误标头的请求在处理程序运行前就被拒绝。 当你准备好执行真实结算时,切换回 `HTTPFacilitatorClient` 并在主网上使用小额金额运行。 :::warning 打桩结算仅验证中间件行为。它无法证明你的路由处理程序在真实网络延迟或并发支付下是幂等的。在发布之前,始终要用小额金额在真实主网上完成测试。 ::: ### 进阶:生命周期钩子 x402 提供钩子,可在流程的关键节点拦截并自定义支付处理。例如,服务端可以在验证之前运行逻辑(如检查 API 密钥或订阅者状态),从而为已授权的请求绕过支付,而客户端可以在签名前强制实施支出限制。 完整的钩子参考和示例,请参阅 [x402 生命周期钩子](https://x402.semanticpay.io/docs/hooks)。 ### 推荐的后续内容 * [**x402 概念**](/cn/explanation/x402) — 了解协议及其适用场景。 * [**ERC-3009**](/cn/explanation/erc-3009) — 回顾 x402 使用的结算标准。 * [**通过 MCP 服务器付费**](/cn/how-to/pay-with-mcp) — 将此 API 封装为 MCP 工具,以便 AI 客户端可以通过提示词调用它。 ## 创建钱包 Stable 钱包是一个符合以太坊标准的密钥对。任何能生成 EVM 账户的钱包库都可以在 Stable 上直接使用,无需修改。本指南展示两种方式:适用于大多数应用的 ethers.js,以及面向需要为代理和支付提供开箱即用自托管层的集成场景的 Tether [WDK(钱包开发工具包)](https://github.com/tetherto/wdk)。 :::note 无需注册,也无需进行 Stable 专属的账户设置。钱包可以立即从[测试网水龙头](/cn/how-to/use-faucet)或主网转账接收 USDT0。 ::: ### 前提条件 * Node.js 20 或更高版本。 ### 方式 1:ethers.js 安装该库并生成密钥对。 ```bash npm install ethers ``` ```typescript // wallet.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); /** Create a new wallet for a new user. */ export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // show to the user once for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address }; } if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase); } ``` ```bash npx tsx wallet.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` :::warning 在生产环境中,切勿以明文形式记录或存储助记词。请将其加密存储,或使用密钥管理器。`ethers.Wallet.createRandom` 每次调用只返回一次助记词——如果丢失,资金将无法找回。 ::: ### 方式 2:Tether WDK WDK 将密钥派生、签名和交易提交封装到单一接口中。当你希望实现自托管而无需重新实现常见账户流程时,它是正确的选择,并且它能直接与 [x402](/cn/how-to/build-pay-per-call) 集成以实现代理支付。 ```bash npm install @tetherto/wdk @tetherto/wdk-wallet-evm ``` ```typescript // wallet-wdk.ts import WDK from "@tetherto/wdk"; import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; function initWdk(seedPhrase: string) { return new WDK(seedPhrase) .registerWallet("stable", WalletManagerEvm, { provider: "https://rpc.testnet.stable.xyz", }); } /** Create a new wallet for a new user. */ export async function createWallet() { const seedPhrase = WDK.getRandomSeedPhrase(); const wdk = initWdk(seedPhrase); const account = await wdk.getAccount("stable", 0); return { account, address: await account.getAddress(), seedPhrase, // show to the user once for backup }; } /** Restore a wallet from a seed phrase (returning user). */ export async function restoreWallet(seedPhrase: string) { const wdk = initWdk(seedPhrase); const account = await wdk.getAccount("stable", 0); return { account, address: await account.getAddress() }; } ``` ```bash npx tsx wallet-wdk.ts ``` ```text Address: 0xAlice...1234 Seed phrase: liberty shoot ... (12 words) ``` ### 为钱包注资 在钱包能够进行交易之前,它需要 USDT0 作为 gas。在测试网上,可从水龙头请求: ```bash open https://faucet.stable.xyz ``` 粘贴地址并选择按钮即可接收 1 个测试网 USDT0(足够进行数千次原生转账)。对于主网,可从任何受支持的交易所或桥转入 USDT0;参见[跨链桥接到 Stable](/cn/explanation/usdt0-bridging)。 ### 查询余额 原生 USDT0 使用 18 位小数。原生余额即为支付 gas 所用的余额。 ```typescript // balance.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const balance = await provider.getBalance("0xYourAddress"); console.log("Balance:", ethers.formatEther(balance), "USDT0"); ``` ```bash npx tsx balance.ts ``` ```text Balance: 1.0 USDT0 ``` ### 推荐的后续步骤 * [**使用 EIP-7702 进行委托**](/cn/how-to/account-abstraction) — 为该钱包添加批量支付、支出限额和会话密钥。 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 在同一余额上进行原生和 ERC-20 转账。 * [**为测试网钱包注资**](/cn/how-to/use-faucet) — 用于获取更大测试余额的水龙头和 Sepolia 桥接选项。 Stable 提供 MCP 服务器、agent skills 和纯文本文档文件,让 AI 编辑器和编码代理能够直接与 Stable 协作。本页介绍如何将每个组件接入你的工作流、为非 MCP AI 工具提供可复制粘贴的上下文块,以及常见任务的入门提示词。 ### MCP 服务器 Stable 运行两个 MCP 服务器。**Docs MCP** 在本文档站点中搜索概念、指南、代码片段和合约参考。**Runtime MCP** 与 Stable 链交互,用于余额查询、交易模拟和执行。 这两个服务器都可以添加到任何兼容 MCP 的客户端。 #### Cursor 打开你的 MCP 配置文件并添加: ```json { "mcpServers": { "stable-docs": { "url": "https://docs.stable.xyz/mcp" }, "stable-runtime": { "url": "https://runtime.stable.xyz/mcp" } } } ``` 重启 Cursor。通过询问以下问题进行验证:"How do I send USDT0 on Stable?" #### Claude Code ```bash claude mcp add stable-docs https://docs.stable.xyz/mcp claude mcp add stable-runtime https://runtime.stable.xyz/mcp ``` 通过询问以下问题进行验证:"Search Stable docs for Gas Waiver integration steps." ### Agent skills Agent skills 是结合了 Docs MCP 和 Runtime MCP 的预定义工作流。当你要求 AI 执行诸如"向三个地址发送 100 USDT0"之类的任务时,该 skill 会处理整个流程:查找相关文档、解析地址和参数、检查余额、模拟交易,并在批准后执行。 Skills 以 Claude Code 插件的形式提供。 #### 安装 ```bash claude plugin add stable-xyz/agent-skills ``` 或从 Claude Code 市场安装。 有关完整的 skill 定义和源代码,请参阅 [agent-skills 仓库](https://github.com/stable-xyz/agent-skills)。 ### 纯文本文档 对于不支持 MCP 的 AI 工具,Stable 文档以静态文本文件的形式提供。 | **文件** | **URL** | **内容** | | :-------------- | :----------------------------------------------------------------------------- | :---------- | | `llms.txt` | [https://docs.stable.xyz/llms.txt](https://docs.stable.xyz/llms.txt) | 带标题和描述的页面索引 | | `llms-full.txt` | [https://docs.stable.xyz/llms-full.txt](https://docs.stable.xyz/llms-full.txt) | 单个文件中的完整文档 | 这些文件是静态快照。如需最新内容,请使用 Docs MCP。 #### Cursor 1. 转到 **Settings > Features > Docs**。 2. 选择 **Add** 并输入 `https://docs.stable.xyz/llms-full.txt`。 3. 在聊天中使用 `@Stable` 引用。 #### 其他工具 下载 `llms-full.txt` 并将其包含在你的项目上下文或系统提示词中。 ### Stable 上下文块 将此内容粘贴到任何 AI 聊天或系统提示词的顶部。它为模型提供了首次尝试即可生成正确 Stable 代码所需的一切。 ```markdown # Stable chain context Stable is a Layer 1 where USDT0 is the native gas token. Fully EVM-compatible. All standard EVM tools (Hardhat, Foundry, ethers.js, viem) work unchanged once you adjust three gas fields (see Behavioral differences below). ## Network | Field | Mainnet | Testnet | | :-------------- | :--------------------------------------- | :----------------------------------------- | | Chain ID | 988 | 2201 | | RPC | https://rpc.stable.xyz | https://rpc.testnet.stable.xyz | | Explorer | https://stablescan.xyz | https://testnet.stablescan.xyz | | Currency symbol | USDT0 | USDT0 | ## USDT0 contract addresses - Mainnet: 0x779ded0c9e1022225f8e0630b35a9b54be713736 - Testnet: 0x78cf24370174180738c5b8e352b6d14c83a6c9a9 ## Behavioral differences from Ethereum 1. **Gas token is USDT0, not ETH.** The `value` field in native transfers carries USDT0. Fees are denominated in USDT0. 2. **`maxPriorityFeePerGas` is always 0.** No tip-based ordering. Set it explicitly to `0n` or validators will reject or ignore tip components. 3. **USDT0 has a dual role**: native asset (18 decimals) AND ERC-20 (6 decimals) on the same balance. `address(x).balance` reports 18-decimal wei; `USDT0.balanceOf(x)` reports 6-decimal units. Values may differ by up to 0.000001 USDT0 due to fractional reconciliation. Never mirror native balance in an internal variable; always query at payout time. 4. **Transfer events are emitted for native transfers too.** A single Transfer event listener on the USDT0 ERC-20 contract covers both transfer paths. 5. **Single-slot finality (~700ms).** Once a block is committed, it cannot be reorged. No need to wait multiple confirmations. 6. **Gas Waiver** lets applications cover gas: user signs with `gasPrice = 0`, a governance-registered waiver wraps and submits. Contracts must be on the waiver's AllowedTarget policy. 7. **EIP-7702** is supported for delegating an EOA to a contract (type-4 tx). 8. **Precompile addresses**: Bank `0x...1003`, Distribution `0x...0801`, Staking `0x...0800`, StableSystem `0x...9999`. ## Common mistakes to avoid - Copying Ethereum priority-fee constants (2 gwei tips, etc.) — has no effect on Stable and can be rejected by wallets. - Using `ethers.parseUnits(x, 18)` for ERC-20 USDT0 amounts. ERC-20 uses 6 decimals; native transfers use 18. - Mirroring native balance in a `uint256 deposited` variable — USDT0 allowance-based operations (transferFrom, permit) can reduce a contract's native balance without invoking its code. - Sending native or ERC-20 USDT0 to `address(0)` — both revert on Stable. - Assuming `EXTCODEHASH == 0` means an address is unused. On Stable, permit-based approvals can change state without incrementing nonce. - Writing `value: ethers.parseEther(amount, "ether")` and expecting ETH semantics. That transfer sends USDT0. ``` ### 入门提示词 加载上面的上下文块后,将以下任意提示词复制到你的 AI 编辑器中。 #### 部署合约 ```text Use Foundry to scaffold a project called `stable-escrow`. Write a minimal Escrow contract in Solidity ^0.8.24 with deposit() and withdraw(amount) functions that transfer USDT0 natively. Use address(this).balance for solvency checks (never mirror the balance in a uint256). Reject address(0) recipients. Then produce a deployment command using `forge create` pointed at Stable testnet (RPC https://rpc.testnet.stable.xyz, chain ID 2201). ``` #### 发送 USDT0 ```text Write a TypeScript script using ethers v6 that sends 0.001 USDT0 natively from the wallet loaded from PRIVATE_KEY. Use base-fee-only EIP-1559 gas (maxPriorityFeePerGas = 0n, maxFeePerGas = 2 * baseFeePerGas). Target Stable testnet. Log the tx hash and a Stablescan explorer URL. ``` #### 设置 EIP-7702 委托 ```text Write a TypeScript script using ethers v6 that: 1. Signs an EIP-7702 authorization delegating my EOA to Multicall3 at 0xcA11bde05977b3631167028862bE2a173976CA11 on Stable testnet (chain ID 2201). 2. Sends a type-4 transaction with authorizationList: [signedAuth], to: wallet.address (self-call), and data that invokes aggregate3() to batch three USDT0 transfers (100, 200, 150 USDT0 with 6 decimals). 3. Use maxPriorityFeePerGas: 0n. ``` #### 构建订阅合约 ```text Write a SubscriptionManager Solidity contract for EIP-7702 delegation on Stable. It runs on a subscriber's EOA. Expose: - subscribe(bytes32 subId, address provider, uint256 amount, uint256 interval) callable only when msg.sender == address(this) (subscriber on their own EOA). - collect(bytes32 subId) callable only by the registered provider, only when block.timestamp >= nextChargeAt; advances nextChargeAt by interval and transfers USDT0 to the provider. Use IERC20 USDT0 at the testnet address 0x78cf24370174180738c5b8e352b6d14c83a6c9a9. - cancelSubscription(bytes32 subId) callable only by the subscriber. Emit events for SubscriptionCreated, SubscriptionCollected, SubscriptionCancelled. ``` #### 构建 x402 按调用付费 API ```text Write an Express server in TypeScript that exposes GET /weather priced at $0.001 USDT0 (amount: "1000", 6 decimals) using @x402/express, @x402/evm/exact/server, and HTTPFacilitatorClient pointed at https://x402.semanticpay.io/. Use Stable mainnet (CAIP-2 eip155:988, USDT0 at 0x779Ded0c9e1022225f8E0630b35a9b54bE713736). The handler should return { weather: "sunny", temperature: 70 }. Read PAY_TO_ADDRESS from env. Print the configured routes on startup. ``` ### 下一步推荐 * [**使用 MCP 服务器付费**](/cn/how-to/pay-with-mcp) — 将付费 API 封装为 MCP 工具,使 AI 客户端能够调用并为其付费。 * [**快速开始**](/cn/tutorial/quick-start) — 将 AI 上下文与五分钟内的首次交易运行配对。 * [**与 Ethereum 的区别**](/cn/explanation/ethereum-comparison) — 深入了解上下文块中的 gas 和 USDT0 语义。 ## 索引合约事件 索引将链上事件转化为应用程序可以响应的数据:余额更新、交易历史、UI 通知。本指南展示如何使用 ethers.js 订阅已部署的 Stable 合约的事件,以及如何回填历史事件,这样你就不会遗漏服务离线期间发出的任何事件。 ### 前置条件 * 在 Stable 测试网或主网上部署的合约。如果你需要一个,请参阅 [部署](/cn/tutorial/smart-contract) 和 [验证](/cn/how-to/verify-contract)。 * Node.js 20 或更高版本。 * 你想要索引的事件的合约地址和 ABI。 ### 1. 安装与配置 ```bash npm install ethers ``` ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const STABLE_TESTNET_WS = "wss://rpc.testnet.stable.xyz"; export const CONTRACT_ADDRESS = "0xDeployedContractAddress"; // Minimal ABI: only the events you want to index. export const CONTRACT_ABI = [ "event NumberUpdated(address indexed caller, uint256 oldValue, uint256 newValue)", ]; ``` ### 2. 订阅实时事件 使用 WebSocket provider,这样你就能在验证者最终确认每个区块后立即收到事件。WebSocket 避免了轮询开销,并使通知延迟接近区块时间(在 Stable 上约 0.7 秒)。 ```typescript // watchLive.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); contract.on("NumberUpdated", (caller, oldValue, newValue, event) => { console.log("NumberUpdated:"); console.log(" caller: ", caller); console.log(" oldValue: ", oldValue.toString()); console.log(" newValue: ", newValue.toString()); console.log(" tx: ", event.log.transactionHash); console.log(" block: ", event.log.blockNumber); }); console.log("Listening for NumberUpdated events..."); ``` ```bash npx tsx watchLive.ts ``` ```text Listening for NumberUpdated events... NumberUpdated: caller: 0x1234...abcd oldValue: 0 newValue: 42 tx: 0x8f3a...2d41 block: 1284371 ``` 当调用者调用你的合约时,事件会实时到达。 ### 3. 回填历史事件 当服务启动时,你通常需要补上离线期间发出的事件。使用带区块范围的 `queryFilter`。 ```typescript // backfill.ts import { ethers } from "ethers"; import { STABLE_TESTNET_RPC, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); const latest = await provider.getBlockNumber(); const fromBlock = Math.max(0, latest - 10_000); // last ~10k blocks const events = await contract.queryFilter( contract.filters.NumberUpdated(), fromBlock, latest ); for (const event of events) { console.log( `[block ${event.blockNumber}]`, event.args.caller, "set number to", event.args.newValue.toString() ); } console.log(`Backfilled ${events.length} events from block ${fromBlock} to ${latest}`); ``` ```bash npx tsx backfill.ts ``` ```text [block 1282351] 0x1234...abcd set number to 10 [block 1283092] 0xef01...2345 set number to 25 [block 1284371] 0x1234...abcd set number to 42 Backfilled 3 events from block 1282351 to 1284371 ``` :::warning 过宽的区块范围(数百万个区块)可能超出 RPC 速率限制并导致超时。对于生产环境的索引器,请按 10k 区块窗口分页,或使用 [Stablescan 的 Etherscan 兼容 API](/cn/how-to/build-p2p-payments#transaction-history) 进行已索引的历史查询。 ::: ### 4. 按索引参数过滤事件 带有 `indexed` 参数的事件(如上面的 `caller`)可以在服务端进行过滤。传入过滤值,而不是读取每个事件后在应用中过滤。 ```typescript // watchUser.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); const userAddress = "0x1234...abcd"; const filter = contract.filters.NumberUpdated(userAddress); contract.on(filter, (caller, oldValue, newValue, event) => { console.log(`${caller} set number to ${newValue.toString()}`); }); console.log(`Watching NumberUpdated for ${userAddress}...`); ``` ```bash npx tsx watchUser.ts ``` ```text Watching NumberUpdated for 0x1234...abcd... 0x1234...abcd set number to 42 ``` ### 处理连接断开 WebSocket 连接可能会断开。对于生产环境的索引器,请实现重连逻辑,以免遗漏事件。 ```typescript // resilientWatch.ts import { ethers } from "ethers"; import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config"; let reconnectAttempts = 0; const MAX_RECONNECT = 5; function setupWatcher() { const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); contract.on("NumberUpdated", (caller, oldValue, newValue) => { console.log(`${caller} set number to ${newValue.toString()}`); }); provider.websocket.onerror = (err: any) => { console.error("Provider error:", err); if (reconnectAttempts < MAX_RECONNECT) { reconnectAttempts++; setTimeout(setupWatcher, 5000); } }; } setupWatcher(); ``` ### 后续推荐 * [**追踪解绑完成**](/cn/how-to/track-unbonding) — 索引协议发出的系统交易事件(解绑完成)。 * [**构建 P2P 支付应用**](/cn/how-to/build-p2p-payments) — 将索引应用于 USDT0 Transfer 事件并构建支付历史视图。 * [**JSON-RPC 参考**](/cn/reference/json-rpc-api) — 查看 Stable 支持哪些 `eth_getLogs` 及相关方法。 ## 索引验证者数据 验证者数据存储在链上,可通过标准 EVM JSON-RPC 读取。你通过 staking、slashing 和 governance 预编译合约查询当前状态,并从它们的事件日志重建历史。这意味着索引器或分析平台可以通过 `eth_call` 和 `eth_getLogs` 读取所需的一切,无需访问节点的 `stabled` CLI 或 Cosmos REST。 :::note **概念:** 关于 staking 模块跟踪的内容以及委托的工作原理,请参阅 [Staking 模块](/cn/explanation/staking-module)。关于每个方法的输入和输出,请参阅 [Staking 预编译参考](/cn/reference/staking-module-api)。 ::: ### 每个数据点的来源 | **数据点** | **来源** | **如何读取** | | :---------- | :------------------------------- | :---------------------------------------------------------------- | | 验证者名称、身份、网站 | Staking 预编译 `validators()` | `description.moniker` 及相关字段 | | 质押(已绑定代币) | Staking 预编译 `validators()` | `tokens` 字段 | | 佣金 | Staking 预编译 `validators()` | `commission` 字段 | | 质押随时间的变化 | Staking 预编译事件 | `Delegate`、`Unbond`、`Redelegate` 日志 | | 加入日期 | Staking 预编译事件 | `CreateValidator` 日志 → 区块时间戳 | | 在线率 | Slashing 预编译 `getSigningInfos()` | `(signedBlocksWindow − missedBlocksCounter) / signedBlocksWindow` | | 投票历史(汇总) | Gov 预编译 `getTallyResult()` | 每个提案的计票 | | 投票历史(按验证者) | Gov 预编译事件 | `Vote`、`VoteWeighted` 日志,voter = operator 地址 | ### 预编译地址 | **模块** | **地址** | **用途** | | :----------- | :------------------------------------------- | :--------------- | | Staking | `0x0000000000000000000000000000000000000800` | 验证者集合、质押、佣金、委托事件 | | Distribution | `0x0000000000000000000000000000000000000801` | 奖励和佣金提取 | | Gov | `0x0000000000000000000000000000000000000805` | 提案、计票和投票日志 | | Slashing | `0x0000000000000000000000000000000000000806` | 签名信息和在线率 | 在 `https://rpc.stable.xyz` 连接到主网(Chain ID `988`)。关于端点和限制,请参阅 [主网信息](/cn/reference/mainnet-information)。 ### 验证者名称、质押和佣金 在 staking 预编译上调用 `validators()` 以读取当前的验证者集合。传入绑定状态进行过滤(例如 `BOND_STATUS_BONDED`)。每个条目都暴露验证者的 `description`(包括 `moniker`)、`tokens`(已绑定质押)和 `commission`。 ```typescript // validators.ts import { createPublicClient, http } from "viem"; const STAKING_PRECOMPILE = "0x0000000000000000000000000000000000000800"; const client = createPublicClient({ transport: http("https://rpc.stable.xyz"), }); // See the staking precompile reference for the full validators() ABI and structs. const validators = await client.readContract({ address: STAKING_PRECOMPILE, abi: stakingAbi, functionName: "validators", args: ["BOND_STATUS_BONDED", { key: "0x", offset: 0n, limit: 100n, countTotal: true, reverse: false }], }); for (const v of validators[0]) { console.log(v.description.moniker, v.tokens.toString(), v.commission.toString()); } ``` ```text StableNode-01 4500000000000000000000000 50000000000000000 StableNode-02 3900000000000000000000000 100000000000000000 ``` `tokens` 和 `commission` 值缩放为 18 位小数。将 `commission` 除以 1e18 即可得到分数形式的费率(例如 `0.05` 表示 5%)。关于完整的 `Validator` 结构体和 `BOND_STATUS_*` 值,请参阅 [Staking 预编译参考](/cn/reference/staking-module-api#validators)。 ### 质押随时间的变化 `validators()` 返回一个快照。要跟踪质押的变动,请索引 staking 预编译的委托事件。`Delegate`、`Unbond` 和 `Redelegate` 携带索引的 `validatorAddr` 和 `amount`,因此你可以将每一次质押变化归因到某个验证者和区块。 ```typescript // stakeChanges.ts import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: STAKING_PRECOMPILE, event: parseAbiItem( "event Delegate(address indexed delegatorAddr, string indexed validatorAddr, uint256 amount, uint256 newShares)" ), fromBlock: 0n, toBlock: "latest", }); console.log(`${logs.length} delegations indexed`); ``` ```text 1842 delegations indexed ``` `Unbond` 和 `Redelegate` 遵循相同的结构,并额外携带 `completionTime`。关于确切的签名,请参阅 staking 参考的 [事件部分](/cn/reference/staking-module-api#events)。 ### 加入日期 验证者的加入日期是其 `CreateValidator` 事件的区块时间戳。该事件按验证者地址索引,因此你可以过滤单个验证者或扫描整个集合,然后用 `eth_getBlockByNumber` 将每个日志的 `blockNumber` 解析为时间戳。 ```typescript // joinDate.ts import { parseAbiItem } from "viem"; const logs = await client.getLogs({ address: STAKING_PRECOMPILE, event: parseAbiItem("event CreateValidator(address indexed valiAddr, uint256 value)"), fromBlock: 0n, toBlock: "latest", }); for (const log of logs) { const block = await client.getBlock({ blockNumber: log.blockNumber }); console.log(log.args.valiAddr, new Date(Number(block.timestamp) * 1000).toISOString()); } ``` ```text 0xAbc...123 2025-11-04T09:12:44.000Z 0xDef...456 2025-12-18T17:03:01.000Z ``` :::warning 创世验证者没有 `CreateValidator` 事件。它们是在创世区块中创建的,而非通过交易,因此不存在日志。将它们的加入日期视为链的创世时间:**2025-10-29**。为所有在创世后加入的验证者索引 `CreateValidator`,并从创世验证者列表回填创世集合。 ::: ### 在线率 使用 `getSigningInfos()` 从 slashing 预编译(`0x...806`)读取签名信息。每条记录报告 `signedBlocksWindow`(滑动窗口的大小)和 `missedBlocksCounter`(窗口内错过的区块)。按如下方式计算在线率: ```text uptime = (signedBlocksWindow − missedBlocksCounter) / signedBlocksWindow ``` `signedBlocksWindow` 为 `10000`、`missedBlocksCounter` 为 `25` 的验证者在窗口内的在线率为 99.75%。这是一个滚动数值,而非终身在线率。要跟踪在线率历史,请按固定间隔对计数器进行快照并存储每次读数。 :::note slashing 预编译遵循 Cosmos EVM `x/slashing` 接口。其地址列在 [系统模块预编译表](/cn/how-to/use-system-modules#whats-exposed) 中。请从链的预编译接口生成确切的方法 ABI。 ::: ### 投票历史 治理数据有两层。对于提案的汇总结果,请在 gov 预编译(`0x...805`)上调用 `getTallyResult()`。对于谁投了什么票,请索引 `Vote` 和 `VoteWeighted` 事件日志。这些日志中的投票者地址是验证者的 operator 地址,因此你可以直接将投票关联到验证者。 ```typescript // votes.ts import { parseAbiItem } from "viem"; const GOV_PRECOMPILE = "0x0000000000000000000000000000000000000805"; const logs = await client.getLogs({ address: GOV_PRECOMPILE, event: parseAbiItem( "event Vote(uint64 indexed proposalId, address indexed voter, uint8 option, uint256 weight)" ), fromBlock: 0n, toBlock: "latest", }); console.log(`${logs.length} votes indexed across all proposals`); ``` ```text 38 votes indexed across all proposals ``` 迄今为止的每个提案(提案 #1 到 #7)都已确认有实时投票日志。当你只需要每个提案的最终计数时,使用 `getTallyResult()`;当你需要每个验证者的记录时,使用事件日志。 :::note gov 预编译遵循 Cosmos EVM `x/gov` 接口。其地址列在 [系统模块预编译表](/cn/how-to/use-system-modules#whats-exposed) 中。请从链的预编译接口生成确切的方法 ABI 和 `VoteOption` 枚举。 ::: ### 推荐的后续步骤 * [**Staking 预编译参考**](/cn/reference/staking-module-api) — 查找完整的 validators()、委托方法和事件签名。 * [**创建验证者**](/cn/how-to/run-validator) — 将已同步的节点注册为验证者,使其出现在上述数据中。 * [**索引器和分析**](/cn/reference/indexers) — 浏览已经提供标准化 Stable 数据的索引提供商。 * [**主网信息**](/cn/reference/mainnet-information) — 在开始索引之前获取 Chain ID、RPC 端点和速率限制。 本指南提供在各种平台上安装和设置 Stable 节点的详细说明。 ### 前提条件 在开始安装之前,请确保你已: * 满足所有[系统要求](/cn/reference/node-system-requirements) * 拥有服务器的 root 或 sudo 访问权限 * 具备基本的 Linux 命令行知识 ### 安装方法 使用适用于你的平台的预编译二进制文件。Stable 目前不支持从源码构建。 #### 主网 ##### Linux AMD64 ```bash # Download the latest binary for AMD64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-amd64-mainnet.tar.gz # Extract the archive tar -xvzf stabled-latest-linux-amd64-mainnet.tar.gz # Move binary to system path sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ##### Linux ARM64 ```bash # Download the binary for ARM64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-arm64-mainnet.tar.gz # Extract and install tar -xvzf stabled-latest-linux-arm64-mainnet.tar.gz sudo mv stabled /usr/bin/ # Verify installation stabled version ``` #### 测试网 ##### Linux AMD64 ```bash # Download the latest binary for AMD64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-amd64-testnet.tar.gz # Extract the archive tar -xvzf stabled-latest-linux-amd64-testnet.tar.gz # Move binary to system path sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ##### Linux ARM64 ```bash # Download the binary for ARM64 architecture wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-arm64-testnet.tar.gz # Extract and install tar -xvzf stabled-latest-linux-arm64-testnet.tar.gz sudo mv stabled /usr/bin/ # Verify installation stabled version ``` ### 节点初始化 安装二进制文件后,初始化你的节点: #### 步骤 1:设置节点名称 ```bash # Set your node's moniker (choose a unique name) export MONIKER="your-node-name" ``` #### 步骤 2:初始化节点 #### 主网 ```bash # Initialize with the mainnet chain ID stabled init $MONIKER --chain-id stable_988-1 # This creates the configuration directory at ~/.stabled/ ``` > **注意**:有关包括 chain ID 在内的当前网络参数,请参阅[主网信息](/cn/reference/mainnet-information) #### 测试网 ```bash # Initialize with the testnet chain ID stabled init $MONIKER --chain-id stabletestnet_2201-1 # This creates the configuration directory at ~/.stabled/ ``` > **注意**:有关包括 chain ID 在内的当前网络参数,请参阅[测试网信息](/cn/reference/testnet-information) #### 步骤 3:下载创世文件 :::code-group ```bash [Mainnet] # Create backup of default genesis mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup # Download mainnet genesis wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/genesis.zip unzip genesis.zip # Move genesis to config directory cp genesis.json ~/.stabled/config/genesis.json # Verify genesis checksum sha256sum ~/.stabled/config/genesis.json # Expected: e1ceda79a3cc48a1028ca8646a2e9e2d156f610637cfb8b428ca8354277921f1 ``` ```bash [Testnet] # Create backup of default genesis mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup # Download testnet genesis wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/genesis.zip unzip genesis.zip # Move genesis to config directory cp genesis.json ~/.stabled/config/genesis.json # Verify genesis checksum sha256sum ~/.stabled/config/genesis.json # Expected: 66afbb6e57e6faf019b3021de299125cddab61d433f28894db751252f5b8eaf2 ``` ::: #### 步骤 4:配置节点 ##### 下载配置文件 :::code-group ```bash [Mainnet] # Download optimized configuration (choose one based on your node type) # For RPC/Full nodes: wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/rpc_node_config.zip unzip rpc_node_config.zip # For Archive nodes: # wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/archive_node_config.zip # unzip archive_node_config.zip # Backup original config cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup # Apply new configuration cp config.toml ~/.stabled/config/config.toml # Update moniker in config sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml ``` ```bash [Testnet] # Download optimized configuration wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/rpc_node_config.zip unzip rpc_node_config.zip # Backup original config cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup # Apply new configuration cp config.toml ~/.stabled/config/config.toml # Update moniker in config sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml ``` ::: ##### 必要的配置更新 编辑 `~/.stabled/config/app.toml`: ```toml # Enable JSON-RPC for EVM compatibility [json-rpc] enable = true address = "0.0.0.0:8545" ws-address = "0.0.0.0:8546" allow-unprotected-txs = true ``` 编辑 `~/.stabled/config/config.toml`: :::code-group ```toml [Mainnet] # P2P Configuration [p2p] # Maximum number of peers max_num_inbound_peers = 50 max_num_outbound_peers = 30 # Seed nodes seeds = "9aa181b20248e948567cb47a15eae35d58cd549d@seed1.stable.xyz:46656" # Persistent peers (mainnet seed nodes) persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656" # Enable peer exchange pex = true # RPC Configuration [rpc] # Listen address laddr = "tcp://0.0.0.0:26657" # Maximum number of simultaneous connections max_open_connections = 900 # CORS settings (adjust for production) cors_allowed_origins = ["*"] ``` ```toml [Testnet] # P2P Configuration [p2p] # Maximum number of peers max_num_inbound_peers = 50 max_num_outbound_peers = 30 # Seed nodes seeds = "6f3195823f7e5ee6f911a0a0ceb9ea689e0dc5bd@seed1.testnet.stable.xyz:56656" # Persistent peers (testnet seed nodes) persistent_peers = "128accd3e8ee379bfdf54560c21345451c7048c7@peer1.testnet.stable.xyz:26656" # Enable peer exchange pex = true # RPC Configuration [rpc] # Listen address laddr = "tcp://0.0.0.0:26657" # Maximum number of simultaneous connections max_open_connections = 900 # CORS settings (adjust for production) cors_allowed_origins = ["*"] ``` ::: ### Systemd 服务设置 创建一个 systemd 服务以实现自动管理: #### 步骤 1:创建服务文件 :::code-group ```bash [Mainnet] sudo tee /etc/systemd/system/stabled.service > /dev/null < /dev/null <> ~/.bashrc echo "export DAEMON_NAME=stabled" >> ~/.bashrc echo "export DAEMON_HOME=$HOME/.stabled" >> ~/.bashrc echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=true" >> ~/.bashrc echo "export DAEMON_RESTART_AFTER_UPGRADE=true" >> ~/.bashrc echo "export DAEMON_LOG_BUFFER_SIZE=512" >> ~/.bashrc echo "export UNSAFE_SKIP_BACKUP=true" >> ~/.bashrc # Load variables source ~/.bashrc ``` #### 步骤 3:设置 Cosmovisor 目录结构 ```bash # Create cosmovisor directory structure mkdir -p ~/.stabled/cosmovisor/genesis/bin mkdir -p ~/.stabled/cosmovisor/upgrades # Copy current binary to genesis cp /usr/bin/stabled ~/.stabled/cosmovisor/genesis/bin/ # Create current symlink ln -s ~/.stabled/cosmovisor/genesis ~/.stabled/cosmovisor/current # Verify setup ls -la ~/.stabled/cosmovisor/ cosmovisor run version ``` #### 步骤 4:设置环境变量 ```bash # Set service name (default: stable) export SERVICE_NAME=stable ``` #### 步骤 5:创建服务文件 :::code-group ```bash [Mainnet] sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null < /dev/null < /dev/null < /dev/null <` ### 概述 集成流程分为三个步骤: 1. **构建 InnerTx**:用户签署一笔 `gasPrice = 0` 的交易。 2. **提交到 Waiver Server**:将已签名的交易提交到 Waiver Server API。 3. **处理响应**:waiver server 包装并广播交易。处理流式返回的结果,并向用户展示交易哈希。 ### 步骤 1:创建用户的 InnerTx 用户签署一笔 `gasPrice = 0` 的标准交易。`to` 地址和方法选择器必须被豁免的 `AllowedTarget` 策略所允许。 ```typescript // config.ts export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet WAIVER_SERVER: "https://waiver.testnet.stable.xyz", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; ``` ```typescript import { ethers } from "ethers"; import { CONFIG } from "./config"; const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ recipientAddress, ethers.parseUnits("0.01", 18) ]); const gasEstimate = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit: gasEstimate, nonce: nonce, chainId: CONFIG.CHAIN_ID, }; const signedInnerTx = await userWallet.signTransaction(innerTx); ``` :::warning `gasPrice` 必须为 `0`。如果非零,waiver server 将拒绝该交易。 ::: ### 步骤 2:提交到 Waiver Server ```typescript import { CONFIG } from "./config"; const API_KEY = process.env.WAIVER_API_KEY; const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTx], }), }); ``` #### 批量提交 你可以在单次请求中提交多笔已签名的交易: ```typescript body: JSON.stringify({ transactions: [signedTx1, signedTx2, signedTx3], }) ``` 每个结果行都包含一个 `index` 字段,对应交易在数组中的位置。 ### 步骤 3:处理响应 响应以 NDJSON(以换行符分隔的 JSON)形式流式返回。每一行对应一笔已提交的交易。 ```typescript const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); if (result.success) { console.log(`tx ${result.index} confirmed: ${result.txHash}`); } else { console.error(`tx ${result.index} failed: ${result.error.message}`); } } } ``` **成功响应:** ```json {"index": 0, "id": "abc123", "success": true, "txHash": "0x..."} ``` **失败响应:** ```json {"index": 1, "id": "def456", "success": false, "error": {"code": "VALIDATION_FAILED", "message": "invalid signature"}} ``` ### 错误代码 | **代码** | **说明** | | :-------------------- | :--------------------- | | `PARSE_ERROR` | 解析交易失败 | | `INVALID_REQUEST` | 请求体格式错误 | | `BATCH_SIZE_EXCEEDED` | 批量大小超出允许的最大值 | | `VALIDATION_FAILED` | 交易验证失败(例如:签名无效、目标不被允许) | | `BROADCAST_FAILED` | 广播到链上失败 | | `RATE_LIMITED` | 超出速率限制 | | `QUEUE_FULL` | 服务器队列已满 | | `TIMEOUT` | 请求超时 | ### API 参考 #### GET `/v1/health` 健康检查端点。认证:无需。 #### POST `/v1/submit` 提交一批已签名的内部交易。认证:必需(Bearer)。 **请求体:** ```json { "transactions": ["0x", "0x"] } ``` 响应以 NDJSON 形式流式返回。每一行对应一笔已提交交易的索引。 #### GET `/v1/submit` 用于流式提交的 WebSocket 接口。认证:必需(Bearer)。 ### 关键要点 * Gas Waiver 是一种服务端集成:你的后端将已签名的用户交易提交到 Waiver Server。用户从不直接与 Waiver Server 交互。 * 用户始终签署 InnerTx,从而保持签名的完整性。豁免方无法修改用户的交易。 * 目标合约必须位于豁免的 `AllowedTarget` 列表中。 ### 后续推荐 * [**零 Gas 交易**](/cn/how-to/zero-gas-transactions) — 查看以演示为核心的流程,以及如何在收据中验证零 Gas。 * [**自托管 Gas Waiver**](/cn/how-to/self-hosted-gas-waiver) — 在不使用托管 API 的情况下运行你自己的豁免服务。 * [**Gas 豁免协议**](/cn/reference/gas-waiver-api) — 完整的包装交易规范和治理模型。 * [**Stable SDK**](/cn/explanation/sdk-overview) — 使用类型化客户端来签署用户交易,随后将其提交到 Waiver Server。 监控 Stable 节点并执行日常维护任务的综合指南。 ### 监控栈概览 #### 推荐技术栈 * **Prometheus**:指标采集 * **Grafana**:可视化与仪表盘 * **AlertManager**:告警路由与管理 * **Node Exporter**:系统指标 * **Loki**:日志聚合(可选) ### 快速监控配置 #### 步骤 1:启用 Prometheus 指标 ```toml # Edit ~/.stabled/config/config.toml [instrumentation] prometheus = true prometheus_listen_addr = ":26660" namespace = "stablebft" ``` 重启节点: ```bash sudo systemctl restart ${SERVICE_NAME} ``` #### 步骤 2:安装 Prometheus ```bash # Download Prometheus wget https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz tar xvf prometheus-2.45.0.linux-amd64.tar.gz sudo mv prometheus-2.45.0.linux-amd64 /opt/prometheus # Create config sudo tee /opt/prometheus/prometheus.yml > /dev/null < /dev/null < 3 | | `stablebft_consensus_block_interval` | 出块时间 | > 10s | | `stablebft_p2p_peers` | 已连接对等节点 | \< 3 | | `stablebft_mempool_size` | 内存池大小 | > 1500 | | `stablebft_mempool_failed_txs` | 失败交易 | > 100/min | #### 系统指标 | 指标 | 描述 | 告警阈值 | | ---------------------------------- | ------- | -------------- | | `node_cpu_seconds_total` | CPU 使用率 | > 80% 持续 5m | | `node_memory_MemAvailable_bytes` | 可用内存 | \< 10% | | `node_filesystem_avail_bytes` | 可用磁盘 | \< 10% | | `node_network_receive_bytes_total` | 网络接收 | > 100MB/s | | `node_disk_io_time_seconds_total` | 磁盘 I/O | > 80% | | `node_load15` | 系统负载 | > CPU 核心数 \* 2 | ### Grafana 仪表盘配置 #### 导入 Stable 仪表盘 ```json { "dashboard": { "title": "Stable Node Monitoring", "panels": [ { "title": "Block Height", "targets": [ { "expr": "stablebft_consensus_height{chain_id=\"stabletestnet_2201-1\"}" } ] }, { "title": "Peers", "targets": [ { "expr": "stablebft_p2p_peers" } ] }, { "title": "Block Time", "targets": [ { "expr": "rate(stablebft_consensus_height[1m]) * 60" } ] }, { "title": "Mempool Size", "targets": [ { "expr": "stablebft_mempool_size" } ] } ] } } ``` #### 自定义仪表盘导入 通过 Grafana UI 导入仪表盘: ```bash # Navigate to Dashboards > Import > Upload JSON file # Or use Dashboard ID in Grafana's dashboard library ``` ### AlertManager 配置 #### 安装 AlertManager ```bash # Download AlertManager wget https://github.com/prometheus/alertmanager/releases/download/v0.26.0/alertmanager-0.26.0.linux-amd64.tar.gz tar xvf alertmanager-0.26.0.linux-amd64.tar.gz sudo mv alertmanager-0.26.0.linux-amd64 /opt/alertmanager # Configure sudo tee /opt/alertmanager/alertmanager.yml > /dev/null < 1500 for: 10m labels: severity: warning annotations: summary: "High mempool size: {{ $value }}" - alert: DiskSpaceLow expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1 for: 5m labels: severity: critical annotations: summary: "Low disk space: {{ $value | humanizePercentage }}" - alert: HighCPUUsage expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 10m labels: severity: warning annotations: summary: "High CPU usage: {{ $value }}%" ``` ### 日志监控 #### Systemd 日志 ```bash # View recent logs sudo journalctl -u ${SERVICE_NAME} -n 100 # Follow logs sudo journalctl -u ${SERVICE_NAME} -f # Filter by time sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" # Export logs sudo journalctl -u ${SERVICE_NAME} --since today > stable-logs-$(date +%Y%m%d).log ``` #### 日志分析脚本 ```bash #!/bin/bash # analyze-logs.sh # Count errors in last hour echo "Errors in last hour:" sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -c ERROR # Show peer connections echo "Peer connections:" sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep "Peer connection" | tail -10 # Check for consensus issues echo "Consensus rounds:" sudo journalctl -u ${SERVICE_NAME} --since "30 minutes ago" | grep -E "enterNewRound|Timeout" | tail -20 # Memory usage patterns echo "Memory warnings:" sudo journalctl -u ${SERVICE_NAME} --since "1 day ago" | grep -i memory ``` #### Loki 配置(可选) ```bash # Install Loki wget https://github.com/grafana/loki/releases/download/v2.9.0/loki-linux-amd64.zip unzip loki-linux-amd64.zip sudo mv loki-linux-amd64 /usr/local/bin/loki # Install Promtail wget https://github.com/grafana/loki/releases/download/v2.9.0/promtail-linux-amd64.zip unzip promtail-linux-amd64.zip sudo mv promtail-linux-amd64 /usr/local/bin/promtail # Configure Promtail sudo tee /etc/promtail-config.yml > /dev/null < ~/reports/daily_$(date +%Y%m%d).log curl -s localhost:26657/status | jq >> ~/reports/daily_$(date +%Y%m%d).log ``` #### 每周维护 ```bash #!/bin/bash # weekly-maintenance.sh # Prune old data stabled prune # Compact database stabled compact # Update peer list wget https://raw.githubusercontent.com/stable-chain/networks/main/testnet/peers.txt cat peers.txt >> ~/.stabled/config/config.toml # Create snapshot (optional) ./create-snapshot.sh # System updates sudo apt update sudo apt upgrade -y # Restart node (during low activity) sudo systemctl restart ${SERVICE_NAME} ``` #### 数据库维护 ```bash # Check database size du -sh ~/.stabled/data/ # Analyze database stabled debug db stats ~/.stabled/data ``` ### 性能监控 #### 资源使用追踪 ```bash #!/bin/bash # track-resources.sh while true; do TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') CPU=$(top -bn1 | grep "stabled" | awk '{print $9}') MEM=$(top -bn1 | grep "stabled" | awk '{print $10}') IO=$(iostat -x 1 2 | tail -n2 | awk '{print $14}') echo "$TIMESTAMP,CPU:$CPU,MEM:$MEM,IO:$IO" >> ~/metrics/resources.csv sleep 60 done ``` #### 查询性能 ```bash # Monitor RPC response times while true; do START=$(date +%s%N) curl -s http://localhost:26657/status > /dev/null END=$(date +%s%N) DIFF=$((($END - $START) / 1000000)) echo "RPC response time: ${DIFF}ms" sleep 5 done ``` ### 监控最佳实践 1. **建立冗余监控** * 使用外部监控服务 * 实施跨节点监控 * 设置死信开关(dead man's switch)告警 2. **预防告警疲劳** * 基于基线调整告警阈值 * 使用告警分组与抑制 * 实施升级策略 3. **数据保留** * 指标至少保留 30 天 * 归档重要日志 * 定期备份监控配置 4. **安全性** * 使用强密码保护 Grafana * 所有端点使用 HTTPS * 限制 Prometheus 的访问 5. **文档** * 记录所有自定义指标 * 为告警维护操作手册(runbook) * 保持仪表盘描述的更新 ### 后续步骤 * [查阅故障排查指南](/cn/how-to/troubleshoot-node) 以解决问题 * [配置升级](/cn/how-to/upgrade-node) 并结合监控 * 根据您的需求设置自定义告警 ## 使用发票付款 本指南演示如何使用 [ERC-3009](/cn/explanation/erc-3009) 在链上结算发票,其中 nonce 由发票元数据确定性派生。该 nonce 将每笔付款与其发票关联起来,并防止重复付款。 :::note **概念:** 关于发票结算模型以及与传统 B2B 开票方式的比较,请参阅[发票结算](/cn/reference/invoices)。 ::: ### 你将构建的内容 一个完整的发票生命周期:买方在链下签署一个 ERC-3009 授权,供应商将其提交到链上,对账过程通过确定性 nonce 将产生的 `AuthorizationUsed` 事件匹配回发票。 #### 演示 ```text step 1. Invoice issued number: INV-2026-001234 amount: 5000 USDT0 dueDate: 2026-04-30 step 2. Buyer signs authorization (off-chain, no gas) nonce: 0xa1b2...c3d4 (from invoice metadata) signature: 0xf0e9...1234 step 3. Vendor submits transferWithAuthorization tx: 0x8f3a...2d41 amount: 5000 USDT0 transferred to vendor step 4. Reconciliation AuthorizationUsed(nonce=0xa1b2...) → invoice INV-2026-001234 Transfer event verified for correct amount and parties ERP: marked PAID at block 1284371 ``` ### 概览 **买方:** ``` ─── Buyer ─────────────────────────────────────────── nonce = getInvoiceNonce(invoice) authorization = { from: buyer, to: vendor, value: amount, nonce, ... } signature = signTypedData(authorization) // Option A: Buyer submits the transaction directly. usdt0.transferWithAuthorization(authorization, signature) // Option B: Buyer sends {authorization, signature} to the vendor. // The vendor (or a facilitator) submits on the buyer's behalf. ``` **供应商:** ``` ─── Vendor ────────────────────────────────────────── // If Option B: submit transferWithAuthorization using the buyer's signature // Reconcile via AuthorizationUsed event on AuthorizationUsed(authorizer, nonce): invoice = nonceToInvoice.get(nonce) transferLog = receipt.logs.find(Transfer matching invoice.buyer, invoice.vendor, invoice.amount) if transferLog: erpSystem.markPaid(invoice.id, txHash, settledAt) ``` ### 配置 ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const EIP712_DOMAIN = { name: "USDT0", version: "1", chainId: CHAIN_ID, verifyingContract: USDT0_ADDRESS, }; export const TRANSFER_WITH_AUTHORIZATION_TYPE = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; export interface Invoice { number: string; // e.g. "INV-2026-001234" vendor: string; // vendor wallet address buyer: string; // buyer wallet address amount: bigint; // amount in USDT0 atomic units (6 decimals) dueDate: number; // Unix timestamp } ``` ### 步骤 1:生成确定性 nonce 买方和供应商都可以根据发票元数据独立计算出相同的 nonce。无需外部注册表。 ```typescript // nonce.ts import { ethers } from "ethers"; import { Invoice } from "./config"; export function getInvoiceNonce(invoice: Invoice): string { return ethers.solidityPackedKeccak256( ["string", "address", "address", "uint256", "uint256"], [ invoice.number, invoice.vendor, invoice.buyer, invoice.amount, invoice.dueDate, ] ); } // Example const invoice: Invoice = { number: "INV-2026-001234", vendor: "0xVendorAddress", buyer: "0xBuyerAddress", amount: ethers.parseUnits("5000", 6), // 5,000 USDT0 dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000), }; const nonce = getInvoiceNonce(invoice); // Same input always produces the same nonce. // This nonce is consumed on-chain upon payment, preventing double payment. ``` ### 步骤 2:签署授权(买方) 买方使用步骤 1 中的确定性 nonce 签署一个 ERC-3009 的 `transferWithAuthorization`。 ```typescript // sign-invoice.ts import { ethers } from "ethers"; import { provider, EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPE, Invoice, } from "./config"; import { getInvoiceNonce } from "./nonce"; const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider); async function signInvoiceAuthorization(invoice: Invoice) { const nonce = getInvoiceNonce(invoice); const gracePeriod = 30 * 24 * 60 * 60; // 30 days after due date const authorization = { from: invoice.buyer, to: invoice.vendor, value: invoice.amount, validAfter: 0, validBefore: invoice.dueDate + gracePeriod, nonce, }; const signature = await buyerWallet.signTypedData( EIP712_DOMAIN, TRANSFER_WITH_AUTHORIZATION_TYPE, authorization ); return { authorization, signature }; } ``` ### 步骤 3:提交交易 根据由谁提交,有两种选择。 #### 选项 A:买方提交 买方直接提交 `transferWithAuthorization` 交易并支付 gas。当买方需要控制付款执行的时间和方式时使用此选项,例如当买方的会计系统需要将 tx hash 与内部审批流程绑定时。 ```typescript // pay.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider); const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", ], buyerWallet, ); async function payInvoice( authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string }, signature: string, ) { const { v, r, s } = ethers.Signature.from(signature); const tx = await usdt0.transferWithAuthorization( authorization.from, authorization.to, authorization.value, authorization.validAfter, authorization.validBefore, authorization.nonce, v, r, s, ); const receipt = await tx.wait(1); console.log("Invoice paid, tx:", receipt.hash); // The nonce is now consumed; the same invoice cannot be paid twice. return { txHash: receipt.hash, blockNumber: receipt.blockNumber }; } ``` #### 选项 B:供应商提交 买方通过 API、电子邮件或任何渠道将 `{authorization, signature}` 发送给供应商。供应商(或协助方)代表买方提交交易,因此买方无需管理 gas。当供应商需要在同一请求流程内获得同步确认时使用此选项。 ```typescript // settle.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS } from "./config"; const vendorWallet = new ethers.Wallet(process.env.VENDOR_KEY!, provider); const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)", ], vendorWallet, ); async function settleInvoice( authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string }, signature: string, ) { const { v, r, s } = ethers.Signature.from(signature); const tx = await usdt0.transferWithAuthorization( authorization.from, authorization.to, authorization.value, authorization.validAfter, authorization.validBefore, authorization.nonce, v, r, s, ); const receipt = await tx.wait(1); console.log("Invoice settled, tx:", receipt.hash); return { txHash: receipt.hash, blockNumber: receipt.blockNumber }; } ``` ### 步骤 4:通过链上事件对账(供应商) 无论由谁提交交易,每笔发票付款都会发出一个携带确定性 nonce 的 `AuthorizationUsed` 事件。供应商监听该事件并通过 nonce 将其匹配到待处理的发票。由于 nonce 是从发票元数据派生的,因此匹配是精确的。 :::note 通过 nonce 匹配可以识别哪张发票已付款,但供应商还应验证同一交易中的 `Transfer` 事件,以确认正确的金额已发送给正确的收款人。下面的代码包含了此项验证。 ::: ```typescript // reconcile.ts import { ethers } from "ethers"; import { provider, USDT0_ADDRESS, Invoice } from "./config"; import { getInvoiceNonce } from "./nonce"; const usdt0 = new ethers.Contract( USDT0_ADDRESS, [ "event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)", "event Transfer(address indexed from, address indexed to, uint256 value)", ], provider, ); // Build a lookup map: nonce -> invoice. // In production, this comes from your invoice database. const invoices: Invoice[] = [ { number: "INV-2026-001234", vendor: "0xVendorAddress", buyer: "0xBuyerAddress", amount: ethers.parseUnits("5000", 6), dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000), }, ]; const nonceToInvoice = new Map(); for (const inv of invoices) { nonceToInvoice.set(getInvoiceNonce(inv), inv); } usdt0.on("AuthorizationUsed", async (authorizer: string, nonce: string, event: any) => { const invoice = nonceToInvoice.get(nonce); if (!invoice) return; // not one of our invoices const receipt = await event.getTransactionReceipt(); const transferLog = receipt.logs .map((log: any) => { try { return usdt0.interface.parseLog(log); } catch { return null; } }) .find( (parsed: any) => parsed?.name === "Transfer" && parsed.args[0].toLowerCase() === invoice.buyer.toLowerCase() && parsed.args[1].toLowerCase() === invoice.vendor.toLowerCase() && parsed.args[2] === invoice.amount ); if (!transferLog) { console.error("No matching Transfer event for invoice:", invoice.number); return; } // All checks passed console.log(`Invoice ${invoice.number} PAID`); console.log(" tx:", receipt.hash); console.log(" settled at block:", receipt.blockNumber); // In production: update your ERP/accounting system here // erpSystem.markPaid(invoice.number, receipt.hash, receipt.blockNumber); }); console.log("Listening for invoice settlements..."); ``` ```bash npx tsx reconcile.ts ``` ```text Listening for invoice settlements... Invoice INV-2026-001234 PAID tx: 0x8f3a...2d41 settled at block: 1284371 ``` ### 处理失败的付款 已提交的 `transferWithAuthorization` 可能因多种原因而回滚。检测并向供应商或买方呈现每种原因,以便重试或关闭发票。 | **回滚原因** | **起因** | **恢复方法** | | :----------------------------------------------- | :------------------------- | :------------------------- | | `FiatTokenV2: invalid signature` | 签名与授权字段不匹配。 | 要求买方在发票数据不变的情况下重新签署。 | | `FiatTokenV2: authorization is used or canceled` | nonce 已被消耗(重复提交)或买方取消了该授权。 | 将发票标记为已付款;通过 nonce 查找原始交易。 | | `FiatTokenV2: authorization is not yet valid` | 在 `validAfter` 之前提交。 | 等待至 `validAfter` 或签发新的授权。 | | `FiatTokenV2: authorization is expired` | 在 `validBefore` 之后提交。 | 签发一个具有更长时间窗口的新授权。 | | `FiatTokenV2: transfer amount exceeds balance` | 买方的 USDT0 余额不足。 | 通知买方为钱包充值,然后用相同的签名重试。 | 捕获回滚并在重试前对其分类。 ```typescript // retry.ts import { ethers } from "ethers"; async function submitWithRetry( submit: () => Promise, ): Promise { try { const tx = await submit(); const receipt = await tx.wait(1); return receipt!.hash; } catch (err: any) { const reason = err?.info?.error?.message || err?.reason || err?.message || ""; if (reason.includes("authorization is used or canceled")) { // Lookup the original tx by AuthorizationUsed event; mark invoice paid. throw new Error("ALREADY_PAID"); } if (reason.includes("authorization is expired")) { throw new Error("AUTHORIZATION_EXPIRED"); } if (reason.includes("invalid signature")) { throw new Error("INVALID_SIGNATURE"); } if (reason.includes("transfer amount exceeds balance")) { throw new Error("INSUFFICIENT_BALANCE"); } throw err; } } ``` :::warning 切勿在未对错误进行分类的情况下重试失败的提交。对已回滚的 transferWithAuthorization 进行盲目重试,可能会在买方充值后通过验证,而这可能与买方的最新意图不符。 ::: ### 接下来推荐 * [**发票结算概念**](/cn/reference/invoices) — 了解确定性 nonce 的对账模型。 * [**ERC-3009**](/cn/explanation/erc-3009) — 回顾此流程背后的签名授权标准。 * [**启用免 gas 交易**](/cn/how-to/integrate-gas-waiver) — 与 Gas Waiver 结合,从结算路径中消除 gas。 ## 通过 MCP 服务器付款 本指南展示如何将启用 x402 的 API 桥接到 [MCP](https://modelcontextprotocol.io) 工具,使 AI 客户端能够通过自然语言提示词调用并为其付费。它基于 [构建按调用付费 API](/cn/how-to/build-pay-per-call) 中的服务器构建。 ### 你将构建的内容 一个将 x402 付费端点封装为工具的 MCP 服务器。AI 客户端输入自然语言提示词,每次工具调用都会触发一次付费的 x402 请求,结算可在 Stablescan 上查看。用户永远不会看到钱包提示。 #### 演示 ```text step 1. User in Claude: "Pull financials for ACME Corp and assess credit risk." step 2. Client calls get_company_financials("ACME") → MCP handler: fetchWithPayment("/financials?ticker=ACME") → 402 Payment Required → sign ERC-3009 → retry → Facilitator settles $0.01 USDT0 on-chain → tx: 0x8f3a...aaaa → 200 OK { revenue, debt_ratio, cash_flow } step 3. Client calls assess_credit_risk(financials) → MCP handler: fetchWithPayment("/credit-risk", POST) → Facilitator settles $0.05 USDT0 on-chain → tx: 0x9bc4...bbbb → 200 OK { score: 72, rating: "moderate" } step 4. Claude responds: "ACME Corp has a credit risk score of 72 (moderate). Revenue is stable but debt-to-equity ratio is elevated at 1.8x..." ``` 两个 `tx` 值均可在 [https://stablescan.xyz](https://stablescan.xyz) 上查看。 :::note **为代理钱包注资**:MCP 服务器使用你控制的助记词来签署支付。在启动服务器之前,请为该钱包在主网上注入 USDT0。至少 `$0.10` 的余额可以支付多次付费调用;`$1.00` 足以进行长时间测试。需要时,可以使用标准的 USDT0 转账向该钱包地址充值。 ::: ### 概述 **MCP 服务器:** ```typescript // --- MCP Server --- // Bridge x402-enabled APIs to MCP tools tools = { "get_company_financials": { handler: (ticker) => fetchWithPayment("https://api.example.com/financials?ticker=" + ticker), }, "assess_credit_risk": { handler: (financials) => fetchWithPayment("https://api.example.com/credit-risk", { method: "POST", body: JSON.stringify({ financials }), }), }, } ``` **用户(通过 AI 客户端):** ``` ─── AI Client ─────────────────────────────────────── User: "Pull financials for ACME Corp and assess their credit risk." Client calls get_company_financials tool → MCP server sends x402 paid request → Facilitator settles USDT0 on-chain → API returns financial data Client calls assess_credit_risk tool with the result → MCP server sends x402 paid request → Facilitator settles USDT0 on-chain → API returns risk assessment → Client responds with the combined result ``` ### 前提条件 * 一个正在运行的 x402 服务器(参见 [构建按调用付费 API](/cn/how-to/build-pay-per-call))。 * 一个兼容 MCP 的 AI 客户端(Claude Desktop、Claude Code 等)。 ### 第 1 步:创建 MCP 服务器 MCP 服务器充当 AI 客户端与启用 x402 的 API 之间的桥梁。每个工具使用 x402 客户端 SDK 发起付费请求并返回结果。 ```bash npm install @modelcontextprotocol/sdk @x402/fetch @x402/evm @tetherto/wdk-wallet-evm ``` ```typescript // mcp-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import WalletManagerEvm from "@tetherto/wdk-wallet-evm"; import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; import { z } from "zod"; // --- Wallet and x402 client --- const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, { provider: "https://rpc.stable.xyz", }).getAccount(0); const client = new x402Client(); registerExactEvmScheme(client, { signer: account }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); // --- x402 API base URL --- const API_BASE = process.env.API_BASE || "http://localhost:4021"; // --- MCP server --- const server = new McpServer({ name: "x402-payments", version: "1.0.0", }); server.tool( "get_company_financials", "Get company financial data by ticker (paid endpoint, $0.01 per call)", { ticker: z.string().describe("Company ticker symbol (e.g. ACME)") }, async ({ ticker }) => { const response = await fetchWithPayment(`${API_BASE}/financials?ticker=${ticker}`); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; }, ); server.tool( "assess_credit_risk", "Assess credit risk from financial data (paid endpoint, $0.05 per call)", { financials: z.string().describe("JSON string of company financial data") }, async ({ financials }) => { const response = await fetchWithPayment(`${API_BASE}/credit-risk`, { method: "POST", headers: { "Content-Type": "application/json" }, body: financials, }); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data) }] }; }, ); server.tool( "check_balance", "Check the USDT0 balance of the payment wallet", {}, async () => { const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736"; const balance = await account.getTokenBalance(USDT0_STABLE); const formatted = (Number(balance) / 1e6).toFixed(2); return { content: [{ type: "text", text: `Wallet balance: ${formatted} USDT0` }], }; }, ); // --- Start --- const transport = new StdioServerTransport(); await server.connect(transport); ``` 每个工具处理程序都会调用 `fetchWithPayment`,它会自动处理完整的 x402 支付流程。AI 客户端只能看到工具名称、描述和参数。 ### 第 2 步:配置你的 AI 客户端 将 MCP 服务器添加到你的 AI 客户端配置中。 **Claude Desktop**(`claude_desktop_config.json`): ```json { "mcpServers": { "x402-payments": { "command": "npx", "args": ["tsx", "/path/to/mcp-server.ts"], "env": { "SEED_PHRASE": "your seed phrase here", "API_BASE": "https://api.example.com" } } } } ``` **Claude Code:** ```bash claude mcp add x402-payments -- npx tsx /path/to/mcp-server.ts ``` 配置完成后,重启你的 AI 客户端。这些工具应该会出现在可用工具列表中。 :::warning MCP 配置中的助记词控制着真实资金。请使用你的操作系统密钥链或密钥管理器安全地存储它,而不是放在明文配置文件中。 ::: ### 第 3 步:输入提示词并使用 配置完成后,AI 客户端可以通过用户的提示词调用付费 API: **用户:**"拉取 ACME Corp 的财务数据并评估他们的信用风险。" 1. 客户端调用 `get_company_financials("ACME")`:通过 x402 支付 $0.01。返回营收、负债率、现金流等。 2. 客户端调用 `assess_credit_risk(financials)`:通过 x402 支付 $0.05。返回风险评分、评级、关键因素。 3. 客户端响应:"ACME Corp 的信用风险评分为 72(中等)。营收稳定,但负债权益比偏高,为 1.8 倍……" 各个工具也可以单独使用: * "拉取 ACME Corp 的财务数据" 调用 `get_company_financials`($0.01)。 * "评估这些数据的信用风险" 调用 `assess_credit_risk`($0.05)。 * "我还剩多少 USDT0?" 调用 `check_balance`。 用户不会与钱包、签名或支付流程交互。MCP 服务器会透明地为每次工具调用处理支付。 ### 支出控制 为防止意外支出,请考虑为 MCP 服务器添加控制措施。 ```typescript const MAX_PER_CALL = 100_000; // $0.10 in base units const MAX_PER_SESSION = 5_000_000; // $5.00 in base units let sessionSpent = 0n; function checkSpendingLimit(amount: bigint) { if (amount > BigInt(MAX_PER_CALL)) { throw new Error(`Amount exceeds per-call limit of $${MAX_PER_CALL / 1e6}`); } if (sessionSpent + amount > BigInt(MAX_PER_SESSION)) { throw new Error(`Session spending limit of $${MAX_PER_SESSION / 1e6} reached`); } sessionSpent += amount; } ``` 这些限制在服务器端运行。AI 客户端无法修改或绕过它们。 ### 接下来推荐 * [**构建按调用付费 API**](/cn/how-to/build-pay-per-call) — 设置此 MCP 服务器所桥接的 x402 服务器。 * [**x402 概念**](/cn/explanation/x402) — 了解这些支付背后的结算协议。 * [**使用 AI 开发**](/cn/how-to/develop-with-ai) — 将 Stable 的文档和运行时 MCP 服务器接入同一个 AI 客户端。 ## 生产环境就绪 在从测试网切换到主网之前,请逐一完成下面的每个部分。 ### 启动前准备 * **网络目标。** 你的应用读取的是主网值,而非测试网:chain ID `988`、RPC `https://rpc.stable.xyz`、浏览器 `https://stablescan.xyz`。完整配置见 [连接](/cn/reference/connect)。 * **合约已验证。** 已部署的合约在 [stablescan.xyz](https://stablescan.xyz) 上完成验证,以便用户和合作伙伴可以检查它们。 * **主网资金路径。** 你有一套书面记录的方式,让生产钱包获取 USDT0:直接获取、通过 LayerZero 跨链或托管方。水龙头仅限测试网。 * **环境隔离。** 密钥、RPC 凭据和签名路径在测试网和主网之间相互隔离。 ### 安全检查 USDT0 的双重角色行为打破了一些从以太坊沿用而来的假设。下面每一项都应当验证。完整列表见 [迁移检查清单](/cn/explanation/usdt0-behavior)。 **偿付能力检查读取真实的原生余额,而非镜像值。** :::warning 在内部变量中跟踪已存入的原生价值是不安全的。外部的 `USDT0.transferFrom` 调用可以在不触发任何合约代码的情况下耗尽合约的原生余额。 ::: ```solidity // SAFE — checks real balance at the moment of transfer function withdraw() external { uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount, "insufficient balance"); payable(msg.sender).call{value: amount}(""); } ``` **基于授权额度的耗尽路径已被测试覆盖。** 每条 `approve` / `transferFrom` / `permit` 路径都有一项试图耗尽合约原生余额的测试。 **向零地址的转账在调用前被拒绝。** :::warning 在 Stable 上,向 `address(0)` 进行的原生转账和 ERC-20 转账都会回滚。请显式校验接收方,否则你的交易将失败。 ::: ```solidity require(recipient != address(0), "zero address recipient"); payable(recipient).call{value: amount}(""); ``` **地址重用检测不依赖于 `EXTCODEHASH`。** 基于 permit 的授权会在不增加 nonce 的情况下改变原生余额,因此 `EXTCODEHASH` 可能在零哈希和空哈希之间来回波动。请改用显式跟踪。 ### 性能与可靠性 * **RPC 冗余。** 生产流量有故障切换方案。第三方提供商列于 [RPC 提供商](/cn/reference/rpc-providers)。 * **Gas 估算。** 交易将 `maxPriorityFeePerGas` 设为 `0`,并根据当前的基础费用计算 `maxFeePerGas`。参见 [Gas 定价](/cn/reference/gas-pricing-api)。 * **出块时间。** 区块大约每 0.7 秒产生一个,并具有单槽最终性。轮询间隔和确认阈值应根据这一节奏进行调整。 * **重试。** 瞬时 RPC 错误应以幂等方式重试。对于涉及资金的敏感流程,应在下游状态变更之前通过收据或日志验证交易已被打包。 ### 运维归属 * **监控。** 如果你运行自己的节点,告警会监视出块、对等节点健康状况和 RPC 延迟;参见 [监控](/cn/how-to/monitor-node)。如果你使用第三方 RPC,请跟踪提供商的 SLA 和故障切换遥测数据。 * **升级。** 协议版本发布会被跟踪,以便节点运营者能够安排升级;参见 [主网版本历史](/cn/reference/mainnet-version-history)。 * **运行手册。** 针对合约暂停、密钥轮换和 RPC 提供商切换都有回滚流程。 ### 支持与升级 * [开发者协助](/cn/reference/developer-assistance):常见问题和参考指引。 * [Discord](https://discord.gg/stablexyz):社区支持和协议更新。 * `bizdev@stable.xyz`:合作与集成洽谈。 ### 推荐后续阅读 * [**USDT0 行为**](/cn/explanation/usdt0-behavior) — 阅读完整的迁移检查清单和合约设计要求。 * [**主网信息**](/cn/reference/mainnet-information) — 查看主网链参数和版本历史。 * [**RPC 提供商**](/cn/reference/rpc-providers) — 选择第三方 RPC 提供商以实现冗余。 * [**监控**](/cn/how-to/monitor-node) — 为出块和 RPC 健康状况接入指标和告警。 验证者是已在链上注册并绑定了质押的同步全节点。你需要先安装并同步节点,然后通过在质押预编译合约(`0x0000000000000000000000000000000000000800`)上调用 `createValidator` 来注册它。本页介绍注册步骤。关于节点本身,请参阅[安装节点](/cn/how-to/install-node)和[节点配置](/cn/reference/node-configuration)。 :::warning 请在节点完全同步后再进行注册。验证者在尚未追上进度时就签名,可能会双重签名并被永久移出验证者集合(被 tombstone)。在开始前,请在 `config.toml` 中将 `double_sign_check_height` 设置为 `2` 或更高(参见[节点配置](/cn/reference/node-configuration#consensus-configuration))。将其设置为 `1` 不会执行任何检查。 ::: ### 前置条件 * 主网(Chain ID `988`)上一个完全同步的全节点。参见[安装节点](/cn/how-to/install-node)。 * 在 `~/.stabled/config/config.toml` 中将 `double_sign_check_height` 设置为 `2` 或更高。 * 已安装 [Foundry](https://book.getfoundry.sh/),用于 `cast`,以调用预编译合约。 * 你的验证者 EVM 地址上已存入质押金额(以 USDT0 计)。 在继续之前,请确认节点已追上进度。`catching_up` 必须为 `false`。 ```bash curl -s localhost:26657/status | jq '.result.sync_info.catching_up' ``` ```text false ``` ### 步骤 1:准备验证者密钥 创建操作员账户,然后读取 `createValidator` 所需的两个值:共识公钥(base64)和验证者的 EVM 地址。 ```bash # Create the validator operator account stabled keys add validator # Consensus public key (base64) — save this stabled comet show-validator | jq .key # Derive the validator's EVM address (0x form) stabled keys parse $(stabled keys show validator -a) ``` ```text "AbCd...base64PubKey...==" # ... # then, evm address is 0xCAEA59C7476C87D0FF6BE6F04DA207601D5BE7D0 ``` :::warning 将 `~/.stabled/config/priv_validator_key.json` 离线备份,并且绝不要用同一密钥运行两个节点。两个实例用同一密钥签名即为双重签名,会导致永久性削减(slash)。 ::: ### 步骤 2:设置环境 ```bash # Staking precompile contract address export STAKING_ADDRESS="0x0000000000000000000000000000000000000800" # Mainnet EVM RPC export RPC_URL="https://rpc.stable.xyz" # Your operator private key and validator EVM address export PRIVATE_KEY="your_private_key_here" export VALIDATOR_ADDRESS="0xYourValidatorAddress" # Consensus pubkey from Step 1 export PUBKEY="AbCd...base64PubKey...==" # Self-delegation amount in wei (18 decimals). 1000000000000000000 = 1 token export AMOUNT="1000000000000000000" ``` ### 步骤 3:创建验证者 在质押预编译合约上调用 `createValidator`。该函数接受一个 `description` 元组、一个 `commissionRates` 元组、最小自委托额、验证者地址、共识公钥以及绑定金额。使用 `cast` 进行编码并发送。 ```bash # createValidator( # (moniker, identity, website, securityContact, details), # (rate, maxRate, maxChangeRate), # minSelfDelegation, validatorAddress, pubkey, value # ) cast send "$STAKING_ADDRESS" \ "createValidator((string,string,string,string,string),(uint256,uint256,uint256),uint256,address,string,uint256)" \ "(\"My Validator\",\"keybase-id\",\"https://example.com\",\"security@example.com\",\"My validator description\")" \ "(100000000000000000,200000000000000000,10000000000000000)" \ "1000000000000000000" \ "$VALIDATOR_ADDRESS" \ "$PUBKEY" \ "$AMOUNT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ```text transactionHash 0x4f...c2 status 1 (success) ``` 佣金元组为 `(rate, maxRate, maxChangeRate)`,每个值均缩放为 18 位小数。该示例设置了 10% 的费率(`100000000000000000`)、20% 的上限以及 1% 的每日最大变动幅度。`maxRate` 和 `maxChangeRate` 在创建时即固定,之后无法修改。调用成功会发出一个 `CreateValidator` 事件。每个字段的详情请参阅[质押预编译参考](/cn/reference/staking-module-api#createvalidator)。 ### 步骤 4:验证 通过从质押预编译合约读回验证者记录,确认验证者已注册并绑定,然后检查它是否正在签名区块。 ```bash # Read your validator's on-chain record cast call "$STAKING_ADDRESS" \ "validator(address)" "$VALIDATOR_ADDRESS" \ --rpc-url "$RPC_URL" # Confirm the node reports validator info curl -s localhost:26657/status | jq '.result.validator_info' ``` ```text # validator() returns the moniker, tokens, commission, and a bonded status (3) # validator_info shows your consensus address with non-zero voting power ``` ### 添加自委托 要在创建后向自己的验证者绑定更多质押,请在同一个预编译合约上调用 `delegate`。 ```bash cast send "$STAKING_ADDRESS" \ "delegate(address,address,uint256)" \ "$VALIDATOR_ADDRESS" "$VALIDATOR_ADDRESS" "$AMOUNT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ```text status 1 (success) ``` ### 注册之后 保持验证者健康并为网络升级做好准备: * **使用 Prometheus 和 Grafana 监控栈监控签名和漏块情况**,详见[监控节点](/cn/how-to/monitor-node)。 * **自动化升级**,这样你就不会错过升级高度。参见[安装节点](/cn/how-to/install-node#cosmovisor-setup-recommended-for-automatic-upgrades)中的 Cosmovisor 设置和[升级节点](/cn/how-to/upgrade-node)。 * **诊断问题**(不同步、不签名),参见[节点故障排查](/cn/how-to/troubleshoot-node)。 ### 推荐的后续步骤 * [**质押预编译参考**](/cn/reference/staking-module-api) — 查阅完整的 createValidator、delegate 和 editValidator 签名及结构体。 * [**节点配置**](/cn/reference/node-configuration) — 在注册前设置 double\_sign\_check\_height 及其他对验证者至关重要的配置。 * [**监控节点**](/cn/how-to/monitor-node) — 跟踪签名、漏块和资源使用情况,以便在削减发生前发现问题。 * [**索引验证者数据**](/cn/how-to/index-validator-data) — 在验证者上线后,从链上读取其质押、在线率和投票历史。 ## 搭配 viem 使用 SDK `@stablechain/sdk` 基于 viem 构建。`createStable` 接受三种签名模式,你可以根据代码运行的位置选择其一:服务端使用私钥、浏览器端使用用户钱包,或使用你已经构建好的 `WalletClient`(例如在 wagmi 应用中)。 本指南将端到端地展示每种模式。 ### 服务端:私钥 `Account` 使用 viem 的 `privateKeyToAccount`,通过后端持有的私钥进行签名。 ```ts import "dotenv/config"; import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const stable = createStable({ network: Network.Mainnet, account, }); const { txHash } = await stable.transfer({ from: account.address, to: "0xRecipient", amount: 5, }); console.log(txHash); ``` ```text 0x8f3a...2d41 ``` ### 浏览器端:来自钱包的 `Transport` 将 `custom(window.ethereum)`(或任意 EIP-1193 provider)作为 `transport` 传入。SDK 会构建 `WalletClient` 并从 provider 读取签名者地址。 ```ts import { createStable, Network } from "@stablechain/sdk"; import { custom } from "viem"; const stable = createStable({ network: Network.Mainnet, transport: custom(window.ethereum), }); const [from] = await window.ethereum.request({ method: "eth_requestAccounts" }); const { txHash } = await stable.transfer({ from, to: "0xRecipient", amount: 5, }); ``` ```text 0x8f3a...2d41 ``` :::warning `transfer`、`bridge` 和 `swap` 会调用 `switchChain` 将钱包切换到正确的网络。如果用户拒绝,SDK 会抛出带有 `phase: "switch_chain"` 的 `StableTransactionError`。请捕获它并向用户提供重试选项。 ::: ### 自带 `WalletClient` 当你已经拥有一个 `WalletClient`(例如来自 wagmi 或自定义签名器)时,可以直接传入。它的优先级高于 `account` 和 `transport`。 ```ts import { createStable, Network } from "@stablechain/sdk"; import { createWalletClient, custom } from "viem"; import { stable as stableChain } from "viem/chains"; const walletClient = createWalletClient({ chain: stableChain, transport: custom(window.ethereum), }); const [from] = await walletClient.requestAddresses(); const stable = createStable({ network: Network.Mainnet, walletClient, }); const { txHash } = await stable.transfer({ from, to: "0xRecipient", amount: 5 }); ``` ```text 0x8f3a...2d41 ``` ### 选择一种模式 | **模式** | **适用场景** | | :------------- | :----------------------------------------------------- | | `account` | 后端服务、脚本、代理——任何你持有私钥的地方。 | | `transport` | 浏览器应用,用户通过 MetaMask 或不依赖 wagmi 的自定义流程签名。 | | `walletClient` | 你已经拥有配置好的 `WalletClient`(wagmi、RainbowKit、ConnectKit)。 | ### 推荐的后续步骤 * [**搭配 wagmi 使用**](/cn/how-to/sdk-with-wagmi) — 通过 wagmi hooks 将 SDK 接入 React 应用。 * [**SDK 参考**](/cn/reference/sdk) — 每个配置字段、方法、枚举和错误类。 * [**SDK 快速入门**](/cn/tutorial/sdk-quickstart) — 在测试网上运行你的第一笔转账、跨链和兑换。 ## 在 wagmi 中使用 SDK `createStable` 接受一个 viem `WalletClient`,而这正是 wagmi 的 `useWalletClient` 所返回的内容。你像往常一样通过 wagmi 连接钱包,然后在钱包客户端发生变化时记忆化一个 `StableClient`。 本指南假设你使用 wagmi v2 和 `@tanstack/react-query`。 ### 1. 配置 wagmi 将 Stable 添加到 wagmi 配置中。viem 为两个网络都提供了链定义。 ```ts import { http, createConfig } from "wagmi"; import { stable as stableMainnet, stableTestnet } from "viem/chains"; import { injected } from "wagmi/connectors"; export const wagmiConfig = createConfig({ chains: [stableMainnet, stableTestnet], connectors: [injected()], transports: { [stableMainnet.id]: http(), [stableTestnet.id]: http(), }, }); ``` ```text WagmiConfig { chains: [988, 2201], connectors: [injected] } ``` ### 2. 构建一个返回 `StableClient` 的 hook 针对当前的 `WalletClient` 记忆化一个 `StableClient`。当钱包客户端标识发生变化时重新创建它。 ```tsx import { useMemo } from "react"; import { useWalletClient } from "wagmi"; import { createStable, Network, type StableClient } from "@stablechain/sdk"; export function useStable(network: Network = Network.Mainnet): StableClient | null { const { data: walletClient } = useWalletClient(); return useMemo(() => { if (!walletClient) return null; return createStable({ network, walletClient }); }, [walletClient, network]); } ``` :::warning 在用户连接之前,`useWalletClient()` 返回 `undefined`。在调用 SDK 方法之前务必进行守卫检查,否则解构得到的 `walletClient` 将为假值,`createStable` 将没有签名者。 ::: ### 3. 在组件中使用它 ```tsx import { useAccount, useChainId } from "wagmi"; import { Network } from "@stablechain/sdk"; import { useStable } from "./useStable"; export function PayButton() { const { address } = useAccount(); const chainId = useChainId(); const stable = useStable(Network.Mainnet); async function onClick() { if (!stable || !address) return; const { txHash } = await stable.transfer({ from: address, to: "0xRecipient", amount: 1, }); console.log("Sent:", txHash); } return ( ); } ``` ```text Sent: 0x8f3a...2d41 ``` ### 4. 在 React 中进行桥接和兑换 同一个 `stable` 实例可处理桥接和兑换。在 effect 或 `useQuery` 中获取报价,然后在点击时执行。 ```tsx const stable = useStable(Network.Mainnet); const onSwap = async () => { if (!stable) return; const quote = await stable.quoteSwap({ fromToken: "0x8a2B28364102Bea189D99A475C494330Ef2bDD0B", toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, }); const { txHash, toAmount } = await stable.swap({ fromToken: quote.fromToken, toToken: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", amount: 100, fromDecimals: 6, quote, }); console.log({ txHash, toAmount }); }; ``` ```text { txHash: "0xabcd...", toAmount: 99.81 } ``` :::note 使用 `useQuery` 缓存报价效果很好:将 `quoteSwap` / `quoteBridge` 作为查询函数传入,并将缓存的 `quote` 转发给 `swap` / `bridge`。当提供了报价时,SDK 会跳过其内部的报价调用。 ::: ### 推荐的下一步 * [**SDK 参考**](/cn/reference/sdk) — 每个方法、配置字段和错误类。 * [**与 viem 一起使用**](/cn/how-to/sdk-with-viem) — 并排比较三种签名模式。 * [**SDK 快速入门**](/cn/tutorial/sdk-quickstart) — 在测试网上运行你的第一笔转账、桥接和兑换。 ## 自托管 gas 豁免 自托管 Gas 豁免让你可以运营自己的豁免基础设施,而不必使用托管的 Waiver Server API。你通过链上治理注册一个豁免地址,然后直接向网络广播包装交易。 本指南涵盖注册豁免地址、收集已签名的用户交易、构造包装交易并广播它们。 :::note **概念:** 关于 Gas 豁免是什么以及为何存在,参见 [Gas 豁免](/cn/explanation/gas-waiver)。关于完整的协议规范(包装交易机制、授权、策略检查、执行语义、安全模型),参见 [Gas 豁免协议](/cn/reference/gas-waiver-api)。 ::: 关于托管的 Waiver Server API 集成路径,参见 [启用免 gas 交易](/cn/how-to/integrate-gas-waiver)。 ### 前置条件 * 通过验证者治理在链上注册的豁免地址。 * 为你的目标合约配置的 `AllowedTarget` 策略。 ### 概述 自托管流程: 1. **从用户收集已签名的 InnerTx**,其 `gasPrice = 0`。 2. **构造 WrapperTx**:对 InnerTx 进行 RLP 编码,并将其包装在一笔发送到 marker 地址的交易中。 3. **广播** WrapperTx,通过 `eth_sendRawTransaction`。 ### 步骤 1:收集用户的 InnerTx 用户签名一笔 `gasPrice = 0` 的交易。`to` 地址和方法选择器必须匹配你豁免的 `AllowedTarget` 策略。 ```typescript // config.ts export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet MARKER_ADDRESS: "0x000000000000000000000000000000000000f333", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; ``` ```typescript // collectInnerTx.ts import { ethers } from "ethers"; import { CONFIG } from "./config"; const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ recipientAddress, ethers.parseUnits("0.01", 18) ]); const gasEstimate = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit: gasEstimate, nonce: nonce, chainId: CONFIG.CHAIN_ID, }; const signedInnerTx = await userWallet.signTransaction(innerTx); ``` ### 步骤 2:构造 WrapperTx 对已签名的 InnerTx 进行 RLP 编码,并将其包装在一笔发送到 marker 地址的交易中。`gasLimit` 必须同时覆盖内部执行和包装开销。 ```typescript // constructWrapper.ts import { ethers } from "ethers"; import { CONFIG } from "./config"; const innerTxBytes = ethers.decodeRlp(signedInnerTx); const rlpEncoded = ethers.encodeRlp(innerTxBytes); const waiverNonce = await provider.getTransactionCount(waiverWallet.address); const wrapperTx = { to: CONFIG.MARKER_ADDRESS, data: rlpEncoded, value: 0, gasPrice: 0, gasLimit: (gasEstimate * 12n / 10n) * 2n, // ~2x inner gas for overhead nonce: waiverNonce, chainId: CONFIG.CHAIN_ID, }; const signedWrapperTx = await waiverWallet.signTransaction(wrapperTx); ``` :::warning `InnerTx.gasPrice` 和 `WrapperTx.gasPrice` 都必须为 `0`。`WrapperTx.value` 也必须为 `0`。如果其中任何条件不满足,验证者将拒绝该交易。 ::: ### 步骤 3:广播 通过标准 JSON-RPC 提交已签名的 WrapperTx。 ```typescript // broadcast.ts const txHash = await provider.send("eth_sendRawTransaction", [signedWrapperTx]); console.log("Wrapper tx broadcast:", txHash); const receipt = await provider.waitForTransaction(txHash); console.log("Confirmed:", receipt.status === 1); ``` ```text Wrapper tx broadcast: 0x... Confirmed: true ``` ### 关键要点 * 自托管豁免需要一个通过链上验证者治理注册的豁免地址。 * WrapperTx 被发送到 marker 地址(`0x...f333`),并以 RLP 编码的 InnerTx 作为数据。 * InnerTx 和 WrapperTx 都必须满足 `gasPrice = 0` 和 `value = 0`。 ### 下一步推荐 * [**Gas 豁免概念**](/cn/explanation/gas-waiver) — 在运行你自己的豁免之前理解其机制。 * [**Gas 豁免协议**](/cn/reference/gas-waiver-api) — 参考完整的协议规范,了解 marker 路由、授权和执行语义。 * [**启用免 gas 交易**](/cn/how-to/integrate-gas-waiver) — 使用托管的 Waiver Server API,而不是自托管。 ## 订阅与收款 本指南将引导你构建一个订阅支付系统,订阅者只需授权一次,服务提供方便可在每个计费周期通过 EIP-7702 账户抽象自动收款。 :::note **概念:** 关于订阅模型、权衡取舍以及与卡片预存计费的比较,请参阅[订阅计费](/cn/reference/subscriptions)。 ::: ### 你将构建的内容 一个完整的订阅生命周期:订阅者委托并订阅一次,提供方按计划收款(展示第二个周期以证明重复行为),订阅者取消订阅。 #### 演示 ```text step 1. Subscriber delegates EOA to SubscriptionManager (EIP-7702) tx: 0x7702...aaaa step 2. Subscriber registers subscription (10 USDT0 / 30 days) subscriptionId: 0xabc... nextChargeAt: 2026-05-23T12:00:00Z step 3. Provider calls collect() on day 30 collected: 10 USDT0 gas cost: ~0.000050 USDT0 nextChargeAt: 2026-06-22T12:00:00Z step 4. Provider calls collect() on day 60 collected: 10 USDT0 gas cost: ~0.000050 USDT0 nextChargeAt: 2026-07-22T12:00:00Z step 5. Subscriber cancels subscription: inactive ``` ### 概述 **订阅者:** ``` ─── Subscriber ─────────────────────────────────────── // One-time setup: delegate EOA to the subscription contract signAuthorization(delegateContract) sendTransaction({ type: 4, authorizationList: [signedAuth] }) // Subscribe: set billing terms on own EOA sendTransaction({ to: self, data: subscribe(subscriptionId, provider, amount, interval) }) // Cancel: revoke billing access at any time sendTransaction({ to: self, data: cancelSubscription(subscriptionId) }) ``` **服务提供方:** ``` ─── Service Provider ──────────────────────────────── // Each billing cycle: collect payment from subscriber's EOA // The delegate contract verifies caller, billing schedule, and amount sendTransaction({ to: subscriberEOA, data: collect(subscriptionId) }) // Automate with a cron job matching the billing interval // The contract reverts if called before the interval has elapsed ``` ### 委托合约 订阅计费的工作原理是将订阅者的 EOA 委托给一个执行计费条款的合约。通过 EIP-7702,订阅者的账户临时获得合约逻辑,使得服务提供方能够在每个计费周期收款,而无需订阅者每次都签名。 你可以使用现有的已部署合约,也可以部署自己的合约。下面的示例是一个最小化的 `SubscriptionManager` 合约,支持三种操作: * `subscribe`:为某个 `subscriptionId` 注册计费条款。 * `collect`:提供方为该 `subscriptionId` 拉取下一笔计划付款。 * `cancelSubscription`:订阅者撤销某个特定订阅。 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SubscriptionManager (example) /// @notice Delegate contract for EIP-7702 subscription billing. /// Runs on the subscriber's EOA via delegation. contract SubscriptionManager { struct Subscription { address provider; uint256 amount; uint256 interval; uint256 nextChargeAt; bool active; } // Keyed by subscriptionId. // Storage is already per subscriber EOA under delegation. mapping(bytes32 => Subscription) public subscriptions; IERC20 public immutable usdt0; event SubscriptionCreated( bytes32 indexed subscriptionId, address indexed provider, uint256 amount, uint256 interval, uint256 nextChargeAt ); event SubscriptionCollected( bytes32 indexed subscriptionId, address indexed provider, uint256 amount, uint256 collectedAt ); event SubscriptionCancelled(bytes32 indexed subscriptionId); constructor(address _usdt0) { usdt0 = IERC20(_usdt0); } /// @notice Register a subscription. Called by the subscriber on their own EOA. function subscribe( bytes32 subscriptionId, address provider, uint256 amount, uint256 interval ) external { require(msg.sender == address(this), "subscriber only"); require(provider != address(0), "invalid provider"); require(amount > 0, "invalid amount"); require(interval > 0, "invalid interval"); require(!subscriptions[subscriptionId].active, "already exists"); uint256 nextChargeAt = block.timestamp + interval; subscriptions[subscriptionId] = Subscription({ provider: provider, amount: amount, interval: interval, nextChargeAt: nextChargeAt, active: true }); emit SubscriptionCreated(subscriptionId, provider, amount, interval, nextChargeAt); } /// @notice Collect a payment for a specific subscription. Called by the service provider. function collect(bytes32 subscriptionId) external { Subscription storage sub = subscriptions[subscriptionId]; require(sub.active, "not active"); require(msg.sender == sub.provider, "not provider"); require(block.timestamp >= sub.nextChargeAt, "too early"); sub.nextChargeAt += sub.interval; require(usdt0.transfer(sub.provider, sub.amount), "transfer failed"); emit SubscriptionCollected(subscriptionId, sub.provider, sub.amount, block.timestamp); } /// @notice Cancel a specific subscription. Called by the subscriber. function cancelSubscription(bytes32 subscriptionId) external { require(msg.sender == address(this), "subscriber only"); require(subscriptions[subscriptionId].active, "not active"); delete subscriptions[subscriptionId]; emit SubscriptionCancelled(subscriptionId); } } ``` :::note 此合约作为参考实现提供,仅用于测试目的。委托合约对订阅者的 EOA 拥有完全的执行权限,因此在生产环境中,请使用经过审计和验证的合约。有关 EIP-7702 委托和安全性的更多背景信息,请参阅 [EIP-7702](/cn/explanation/eip-7702)。 ::: ### 配置 ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz"; export const CHAIN_ID = 2201; export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9"; export const SUBSCRIPTION_MANAGER = "0xYourDeployedSubscriptionManager"; export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC); export const subscriberWallet = new ethers.Wallet(process.env.SUBSCRIBER_KEY!, provider); ``` ### 步骤 1:委托订阅者的 EOA(EIP-7702) 订阅者签署一个 EIP-7702 授权,将其 EOA 委托给 `SubscriptionManager`。此后,订阅者的 EOA 将执行委托合约的逻辑。 ```typescript // delegate.ts import { subscriberWallet, provider, CHAIN_ID, SUBSCRIPTION_MANAGER } from "./config"; const authorization = { chainId: CHAIN_ID, address: SUBSCRIPTION_MANAGER, nonce: await provider.getTransactionCount(subscriberWallet.address), }; const signedAuth = await subscriberWallet.signAuthorization(authorization); const tx = await subscriberWallet.sendTransaction({ type: 4, to: subscriberWallet.address, authorizationList: [signedAuth], maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Delegation tx:", receipt.hash); ``` ```bash npx tsx delegate.ts ``` ```text Delegation tx: 0x7702...aaaa ``` ### 步骤 2:注册订阅(订阅者) 订阅者在自己的 EOA 上调用 `subscribe()`。由于 EOA 已被委托,此操作将执行 `SubscriptionManager.subscribe`。 ```typescript // subscribe.ts import { ethers } from "ethers"; import { subscriberWallet } from "./config"; const subscriptionManager = new ethers.Interface([ "function subscribe(bytes32 subscriptionId, address provider, uint256 amount, uint256 interval)", ]); const serviceProvider = "0xServiceProviderAddress"; const monthlyAmount = ethers.parseUnits("10", 6); // 10 USDT0 const interval = 30 * 24 * 60 * 60; // 30 days in seconds // Derive a unique subscriptionId from provider + plan name + local nonce const subscriptionId = ethers.solidityPackedKeccak256( ["address", "string", "uint256"], [serviceProvider, "pro-monthly", 1] ); const tx = await subscriberWallet.sendTransaction({ to: subscriberWallet.address, // call self (delegate code executes) data: subscriptionManager.encodeFunctionData("subscribe", [ subscriptionId, serviceProvider, monthlyAmount, interval, ]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Subscription registered, tx:", receipt.hash); console.log("Subscription ID:", subscriptionId); ``` ```bash npx tsx subscribe.ts ``` ```text Subscription registered, tx: 0xabcd...1234 Subscription ID: 0xfedc...9876 ``` ### 步骤 3:收取付款(服务提供方) 每个计费周期,服务提供方在订阅者的 EOA 上调用 `collect(subscriptionId)`。委托逻辑在转移 USDT0 之前会验证调用者、计费计划和金额。 ```typescript // collect.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const providerWallet = new ethers.Wallet(process.env.PROVIDER_KEY!, provider); const subscriptionManager = new ethers.Interface([ "function collect(bytes32 subscriptionId)", ]); const subscriberEOA = "0xSubscriberEOAAddress"; const subscriptionId = "0xYourSubscriptionId"; const tx = await providerWallet.sendTransaction({ to: subscriberEOA, // subscriber's EOA (runs delegate code) data: subscriptionManager.encodeFunctionData("collect", [subscriptionId]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Payment collected, tx:", receipt.hash); console.log("Gas used:", receipt.gasUsed.toString()); // In production, run this on a cron schedule matching the billing interval. // The delegate contract will revert if called before the interval has elapsed. ``` ```bash npx tsx collect.ts ``` ```text Payment collected, tx: 0x8f3a...2d41 Gas used: 52000 ``` 在 Stable 上,一次 `collect()` 调用大约消耗 **50k-55k gas**(21k 基础 + 7702 委托开销 + ERC-20 `transfer`)。按 1 gwei 基础费用计算,提供方每个计费周期支付的费用约为 `0.000050 USDT0`。 ### 步骤 4:取消订阅(订阅者) 订阅者在自己的 EOA 上调用 `cancelSubscription(subscriptionId)`,以撤销该特定订阅的计费权限。 ```typescript // cancel.ts import { ethers } from "ethers"; import { subscriberWallet } from "./config"; const subscriptionManager = new ethers.Interface([ "function cancelSubscription(bytes32 subscriptionId)", ]); const subscriptionId = "0xYourSubscriptionId"; const tx = await subscriberWallet.sendTransaction({ to: subscriberWallet.address, data: subscriptionManager.encodeFunctionData("cancelSubscription", [subscriptionId]), maxPriorityFeePerGas: 0n, }); const receipt = await tx.wait(1); console.log("Subscription cancelled, tx:", receipt.hash); ``` ```bash npx tsx cancel.ts ``` ```text Subscription cancelled, tx: 0xdef0...5678 ``` ### 安全模型 订阅者授权委托合约从其 EOA 中拉取资金。请准确了解该授权涵盖的范围以及如何限制风险敞口。 **订阅者授权的内容。** 通过委托给 `SubscriptionManager`,订阅者授予该合约逻辑对其 EOA 的完全执行权限。委托方只能在其代码所设定的条件下转移资金:调用者是已注册的提供方、间隔时间已过、金额与已存储的订阅相匹配。它无法转移到其他地址或绕过间隔检查,因为合约代码不允许这些操作。 **需要缓解的失败模式。** * **恶意委托升级**:如果 `SubscriptionManager` 是一个其实现可被管理员更改的代理合约,那么该授权实际上信任了该管理员。仅委托给不可变的合约或具有透明、时间锁定升级的代理合约。 * **提供方被攻破**:如果提供方的密钥泄露,攻击者可在每个周期金额的上限内提前收款。订阅者应为每个订阅设置 `spendingLimit`,并监控未经授权的 `SubscriptionCollected` 事件。 * **委托替换**:使用不同的委托方再次订阅会清除订阅状态。请使用模块化委托方,在单一委托下支持多种功能(订阅、批量支付、支出限额),而不是每个功能使用一个委托方。 * **可重放签名**:所有签名都使用与订阅者 EOA 绑定的 EIP-7702 nonce,因此它们无法跨链或跨委托重放。 **推荐的防护措施。** * 在生产环境使用前审计委托合约。 * 相对于订阅者的余额,保持每个订阅的金额较小。 * 监控 `SubscriptionCreated` / `SubscriptionCollected` 事件并向订阅者展示。 * 为订阅者提供清晰的"取消"界面,在其自己的 EOA 上调用 `cancelSubscription(subscriptionId)`。 ### 重要注意事项 * **持久委托**:EIP-7702 委托会持续存在,直到订阅者明确更改或清除它。无需每个计费周期重新委托。 * **每个 EOA 单一委托**:如果订阅者之后委托给不同的合约,订阅委托逻辑将被替换,收款将失败。请使用模块化委托合约,在单一委托下支持多种功能(订阅、批量支付、支出限额、会话密钥)。 * **计划行为**:此示例在每次成功收款时将 `nextChargeAt` 推进一个间隔。如果已经过去了多个计费周期,重复的 `collect()` 调用可以逐个周期追赶。如果你的产品需要不同的策略,请扩展该逻辑。 * **使用经过审计的委托方**:仅委托给已经过审计的合约。 ### 下一步推荐 * [**订阅计费概念**](/cn/reference/subscriptions) — 了解基于拉取的计费模型。 * [**账户抽象**](/cn/how-to/account-abstraction) — 了解批量支付、支出限额和会话密钥如何在单一委托下组合。 * [**EIP-7702 概念**](/cn/explanation/eip-7702) — 回顾使这一切成为可能的委托模型。 ## 追踪解绑完成 当解绑周期完成时,协议会通过系统交易,借助 `StableSystem` 预编译合约(`0x0000000000000000000000000000000000009999`)发出 `UnbondingCompleted` 事件。这使得 dApp 能够实时通知用户并更新余额,而无需运行自定义索引器或轮询 REST 端点。 :::note **概念:** 关于系统交易如何将 SDK 层事件桥接到 EVM 以及其重要性,请参阅 [系统交易](/cn/explanation/system-transactions)。 ::: ### 前置条件 * 了解 [系统交易](/cn/explanation/system-transactions)。 * 熟悉 [质押](/cn/explanation/staking-module),特别是 `undelegate` 和解绑流程。 * 具备使用标准 web3 库(例如 [ethers.js](https://docs.ethers.org/) v6)订阅和筛选合约事件的经验。 ### 概述 * **设置合约实例**:为 StableSystem 预编译合约创建一个合约实例。 * **在应用中处理事件**:根据应用逻辑订阅实时事件或查询历史数据。 * **处理连接问题**:为持久化的 WebSocket 订阅实现重连逻辑。 ### 步骤 1:设置合约实例 使用 `UnbondingCompleted` 事件 ABI 为 `StableSystem` 预编译合约创建一个合约实例。 ```typescript // config.ts import { ethers } from "ethers"; export const STABLE_SYSTEM_ADDRESS = "0x0000000000000000000000000000000000009999"; export const STABLE_SYSTEM_ABI = [ "event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)", ]; export const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); export const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); ``` ### 步骤 2:在应用中处理事件 根据应用逻辑订阅实时事件、查询历史数据,或两者兼有。 #### 实时订阅 订阅 `UnbondingCompleted` 事件,以便在任何解绑完成时获得实时通知。这对于触发余额更新、发送通知或刷新仪表盘统计数据很有用。 ```typescript // subscribeBasic.ts import { stableSystem } from "./config"; stableSystem.on("UnbondingCompleted", (delegator, validator, amount, event) => { console.log("Unbonding completed:"); console.log(" Delegator:", delegator); console.log(" Validator:", validator); console.log(" Amount:", ethers.formatEther(amount), "tokens"); console.log(" Block:", event.log.blockNumber); console.log(" Tx Hash:", event.log.transactionHash); }); ``` #### 按用户筛选 如果只想接收特定委托人地址的事件,请使用已索引的事件参数创建一个筛选器。 ```typescript // subscribeByUser.ts import { ethers } from "ethers"; import { stableSystem } from "./config"; const userAddress = "0xabcd..."; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { refreshUserBalance(userAddress); showNotification( `Your unbonding of ${ethers.formatEther(amount)} tokens completed!` ); }); ``` #### 按验证人筛选 ```typescript // subscribeByValidator.ts import { stableSystem } from "./config"; const validatorAddress = "0x1234..."; const validatorFilter = stableSystem.filters.UnbondingCompleted( null, validatorAddress ); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### 历史查询 如果你的 dApp 需要展示过去解绑完成的历史记录,请使用带区块范围的事件筛选器查询历史事件。 ```typescript // queryHistory.ts import { ethers } from "ethers"; import { provider, stableSystem } from "./config"; async function getUnbondingHistory( userAddress: string, fromBlock: number, toBlock: number ) { const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter(filter, fromBlock, toBlock); return events.map((event) => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash, })); } const currentBlock = await provider.getBlockNumber(); const history = await getUnbondingHistory( "0xabcd...", currentBlock - 1000, currentBlock ); ``` ### 步骤 3:处理连接问题 事件订阅依赖于持久化的 WebSocket 连接。请为生产环境的 dApp 实现重连逻辑。 ```typescript // subscribeWithReconnection.ts import { ethers } from "ethers"; import { STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI } from "./config"; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function handleUnbonding(delegator: string, validator: string, amount: bigint) { console.log("Unbonding completed:", { delegator, validator, amount }); } function setupEventListener() { const wsProvider = new ethers.WebSocketProvider("wss://rpc.testnet.stable.xyz"); wsProvider.on("error", (error) => { console.error("Provider error:", error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, wsProvider ); stableSystem.on("UnbondingCompleted", handleUnbonding); } setupEventListener(); ``` ### 后续推荐 * [**系统交易概念**](/cn/explanation/system-transactions) — 了解协议层事件如何到达 EVM。 * [**质押模块概念**](/cn/explanation/staking-module) — 回顾委托和解绑流程。 * [**质押预编译参考**](/cn/reference/staking-module-api) — 查找触发此处所追踪事件的方法。 本综合指南帮助诊断和解决 Stable 节点的常见问题。 ### 快速诊断 #### 节点健康检查脚本 ```bash #!/bin/bash # quick-diagnosis.sh # Set service name (default: stable) export SERVICE_NAME=stable echo "=== Stable Node Diagnostics ===" echo "Timestamp: $(date)" echo "" # 1. Service Status echo "1. SERVICE STATUS:" systemctl status ${SERVICE_NAME} --no-pager | head -10 # 2. Sync Status echo -e "\n2. SYNC STATUS:" curl -s localhost:26657/status | jq '.result.sync_info' 2>/dev/null || echo "RPC not responding" # 3. Peer Connections echo -e "\n3. PEER COUNT:" curl -s localhost:26657/net_info | jq '.result.n_peers' 2>/dev/null || echo "Cannot get peer info" # 4. Recent Errors echo -e "\n4. RECENT ERRORS (last 20):" sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -i error | tail -20 # 5. System Resources echo -e "\n5. SYSTEM RESOURCES:" df -h / | grep -v Filesystem free -h | grep Mem top -bn1 | grep "load average" # 6. Port Status echo -e "\n6. PORT STATUS:" ss -tulpn | grep ${SERVICE_NAME} || echo "No ${SERVICE_NAME} ports found" echo -e "\n=== Diagnostics Complete ===" ``` ### 常见问题与解决方案 #### 节点无法启动 ##### 问题:找不到二进制文件 **错误信息:** ``` stabled: command not found ``` **解决方案:** ```bash # Check if binary exists ls -la /usr/bin/stabled # If missing, reinstall (use arm64 if needed) wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz tar -xvzf stabled-0.7.2-linux-amd64-testnet.tar.gz sudo mv stabled /usr/bin/ sudo chmod +x /usr/bin/stabled ``` ##### 问题:权限被拒绝 **错误信息:** ``` Error: open /home/user/.stabled/config/config.toml: permission denied ``` **解决方案:** ```bash # Fix ownership sudo chown -R $USER:$USER ~/.stabled/ # Fix permissions chmod 700 ~/.stabled/ chmod 600 ~/.stabled/config/*.json chmod 644 ~/.stabled/config/*.toml ``` ##### 问题:地址已被占用 **错误信息:** ``` Error: listen tcp 0.0.0.0:26657: bind: address already in use ``` **解决方案:** ```bash # Find process using port sudo lsof -i :26657 # Kill the process sudo kill -9 # Or change port in config sed -i 's/laddr = "tcp:\/\/0.0.0.0:26657"/laddr = "tcp:\/\/0.0.0.0:26658"/' ~/.stabled/config/config.toml ``` #### 同步问题 ##### 问题:节点卡在某个高度 **症状:** * 区块高度不增加 * 超过 1 分钟没有新区块 **解决方案:** ```bash # 1. Check peers curl localhost:26657/net_info | jq '.result.n_peers' # If no peers, add persistent peers echo "persistent_peers = \"5ed0f977a26ccf290e184e364fb04e268ef16430@37.187.147.27:26656,128accd3e8ee379bfdf54560c21345451c7048c7@37.187.147.22:26656\"" >> ~/.stabled/config/config.toml # 2. Reset and resync sudo systemctl stop ${SERVICE_NAME} stabled comet unsafe-reset-all --keep-addr-book sudo systemctl start ${SERVICE_NAME} # 3. Use snapshot (see Snapshots guide) ``` ##### 问题:"wrong Block.Header.AppHash" 错误 **错误信息:** ``` panic: Wrong Block.Header.AppHash. Expected XXXX, got YYYY ``` **解决方案:** ```bash # This indicates state corruption - rollback to previous block sudo systemctl stop ${SERVICE_NAME} # Rollback one block stabled rollback # Restart node sudo systemctl start ${SERVICE_NAME} # If rollback doesn't work, restore from snapshot # Backup important files cp ~/.stabled/config/priv_validator_key.json ~/backup/ cp ~/.stabled/config/node_key.json ~/backup/ # Reset state stabled comet unsafe-reset-all # Restore from snapshot wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 tar -I lz4 -xf snapshot.tar.lz4 -C ~/.stabled/ sudo systemctl start ${SERVICE_NAME} ``` ##### 问题:同步速度慢 **症状:** * 每分钟少于 100 个区块 * CPU/磁盘使用率高 **解决方案:** ```bash # 1. Check disk I/O iostat -x 1 5 # 2. Optimize configuration cat >> ~/.stabled/config/config.toml <> ~/.stabled/config/config.toml < db_dump.txt # 4. If repair fails, resync rm -rf ~/.stabled/data # Restore from snapshot # 5. Start node sudo systemctl start ${SERVICE_NAME} ``` ##### 问题:"too many open files" **错误信息:** ``` accept: too many open files ``` **解决方案:** ```bash # 1. Check current limits ulimit -n # 2. Increase limits echo "* soft nofile 65535" | sudo tee -a /etc/security/limits.conf echo "* hard nofile 65535" | sudo tee -a /etc/security/limits.conf # 3. Update systemd service sudo sed -i '/\[Service\]/a LimitNOFILE=65535' /etc/systemd/system/stabled.service # 4. Reload and restart sudo systemctl daemon-reload sudo systemctl restart ${SERVICE_NAME} ``` #### 内存问题 ##### 问题:内存不足(OOM)导致进程被杀 **症状:** ``` stabled.service: Main process exited, code=killed, status=9/KILL ``` **解决方案:** ```bash # 1. Check memory usage free -h dmesg | grep -i "killed process" # 2. Add swap space sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 3. Optimize memory usage cat >> ~/.stabled/config/app.toml < $OUTPUT_DIR/system.txt df -h >> $OUTPUT_DIR/system.txt free -h >> $OUTPUT_DIR/system.txt # Service status systemctl status ${SERVICE_NAME} --no-pager > $OUTPUT_DIR/service-status.txt # Recent logs sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" > $OUTPUT_DIR/recent-logs.txt # Config files (remove sensitive data) grep -v "priv" ~/.stabled/config/config.toml > $OUTPUT_DIR/config.toml grep -v "priv" ~/.stabled/config/app.toml > $OUTPUT_DIR/app.toml # Node status curl -s localhost:26657/status > $OUTPUT_DIR/node-status.json 2>/dev/null # Create archive tar -czf $OUTPUT_DIR.tar.gz $OUTPUT_DIR/ echo "Debug info collected: $OUTPUT_DIR.tar.gz" echo "Share this file when requesting support" ``` ### 后续步骤 * 查看[监控设置](/cn/how-to/monitor-node)以预防问题 * 查看[升级指南](/cn/how-to/upgrade-node)了解特定版本的问题 本指南涵盖 Stable 节点的升级过程,包括升级流程和回滚策略。 > 有关完整的版本历史和升级详情,请参阅[版本历史](/cn/reference/testnet-version-history)。 ### 升级类型 #### 软升级(非破坏性) * 可随时执行 * 向后兼容 #### 硬升级(破坏性) * 需要在特定高度升级 * 不向后兼容 #### 紧急升级 * 关键安全修复 * 需要立即采取行动 * 可能需要停链 ### 标准升级流程 #### 步骤 1:准备 ```bash # Check current version stabled version --long # Backup critical data cp -r ~/.stabled/config ~/stable-backup-$(date +%Y%m%d)/ # For validators only: Backup validator state cp ~/.stabled/data/priv_validator_state.json ~/stable-backup-$(date +%Y%m%d)/ # Check disk space (need 2x current data size) df -h ~/.stabled ``` #### 步骤 2:下载新二进制文件 ```bash # For v1.2.0-rc1 upgrade (January 22, 2026) # Choose your architecture: # Linux AMD64 BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz" # OR Linux ARM64 BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz" # Download new binary wget $BINARY_URL # Extract to temporary location tar -xvzf stabled-1.2.0-rc1-linux-*.tar.gz -C /tmp/ # Verify new version /tmp/stabled version --long ``` #### 步骤 3:执行升级 ##### 软升级 ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Backup current binary sudo mv /usr/bin/stabled /usr/bin/stabled.backup # Install new binary sudo mv /tmp/stabled /usr/bin/stabled sudo chmod +x /usr/bin/stabled # Verify installation stabled version --long # Start node sudo systemctl start ${SERVICE_NAME} # Monitor logs sudo journalctl -u ${SERVICE_NAME} -f ``` ##### 硬升级 ```bash # Monitor for upgrade height while true; do HEIGHT=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current height: $HEIGHT" if [ $HEIGHT -ge $UPGRADE_HEIGHT ]; then break fi sleep 10 done # Node will halt automatically at upgrade height # Wait for halt message in logs sudo journalctl -u ${SERVICE_NAME} -f | grep "UPGRADE" # Once halted, perform upgrade sudo systemctl stop ${SERVICE_NAME} sudo mv /usr/bin/stabled /usr/bin/stabled.backup sudo mv /tmp/stabled /usr/bin/stabled # Start with new binary sudo systemctl start ${SERVICE_NAME} ``` #### 步骤 4:升级后验证 ```bash # Check node status curl -s localhost:26657/status | jq '.result' # Verify version curl -s localhost:26657/status | jq '.result.node_info.version' # Check peers curl -s localhost:26657/net_info | jq '.result.n_peers' # Monitor sync status watch -n 2 'curl -s localhost:26657/status | jq ".result.sync_info"' # Check for errors sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep -i error ``` ### Cosmovisor 设置(自动升级) Cosmovisor 可为协调升级自动化升级过程。 #### 安装 ```bash # Install cosmovisor go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest # Or download binary wget https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.0/cosmovisor-v1.7.0-linux-amd64.tar.gz tar -xzf cosmovisor-v1.7.0-linux-amd64.tar.gz sudo mv cosmovisor /usr/bin/ ``` #### 配置 ```bash # Set environment variables cat >> ~/.bashrc < /dev/null < > export.json # 3. Wait for coordinated restart instructions ``` ### 后续步骤 * [版本历史](/cn/reference/testnet-version-history) - 完整的升级历史和发布说明 * 升级后[监控你的节点](/cn/how-to/monitor-node) * 查阅[故障排查](/cn/how-to/troubleshoot-node)了解常见问题 ## 如何为您的 Stable 测试网钱包充值 Stable 使用 USDT0作为燃料代币,因此您需要在钱包中有 USDT0 才能与链进行交互。首先,您需要使用水龙头为您的账户充值 USDT0。 1. 访问 [https://faucet.stable.xyz](https://faucet.stable.xyz) 2. 点击"Get USDT0"按钮,1 USDT0 将被投放到您的钱包中。 如果您想要更多 USDT0,您可以从以太坊 Sepolia 将测试 USDT 桥接到 Stable 测试网。 1. 访问 [https://sepolia.etherscan.io/token/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract,通过调用](https://sepolia.etherscan.io/token/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract,通过调用) `_mint` 函数将所需数量的测试 Tether USD 铸造到您的账户中。 2. 向以太坊 Sepolia 上的 LayerZero 桥接合约发送以下交易,将测试 USDT 桥接到 Stable 测试网: ```jsx export function addrTo32Bytes(addr: string): Buffer { const hex20 = ethers.utils.getAddress(addr).slice(2); const padded = hex20.padStart(64, "0"); // 32 bytes ⇒ 64 hex return Buffer.from(padded, "hex"); // length === 32 } async function main() { const [owner] = await ethers.getSigners(); const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927"; const SEPOLIA_USDT0_OAPP = "0xc099cD946d5efCC35A99D64E808c1430cEf08126" const RECEIVER_EID = 40374; const usdt0 = await ethers.getContractAt("ERC20", SEPOLIA_USDT0); await usdt0.approve(SEPOLIA_USDT0_OAPP, ethers.utils.parseEther("1")); const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(); const amount = ethers.utils.parseEther("1"); // 将此更改为您所需的数量 const OFTAdapter = await ethers.getContractAt("OFTAdapter", SEPOLIA_USDT0_OAPP); const sendParams = { dstEid: RECEIVER_EID, to: addrTo32Bytes(owner.address), amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: Buffer.from(""), oftCmd: Buffer.from(""), }; const fee = await OFTAdapter.quoteSend(sendParams, false); await OFTAdapter.send( sendParams, fee, owner.address, { value: fee.nativeFee, } ) } ``` 本指南介绍了使用快照和状态同步快速同步 Stable 节点的各种方法。 ### 同步方法概览 | 方法 | 同步时间 | 所需存储 | 使用场景 | | -------- | ------- | -------- | ---------- | | **裁剪快照** | \~10 分钟 | \< 5 GiB | 常规全节点 | | **归档快照** | \~1 小时 | \~500 GB | 归档节点、区块浏览器 | ### 官方快照 Stable 提供每日更新的官方快照(00:00 UTC)。 #### 快照信息 #### 主网 | 类型 | 压缩格式 | 大小 | URL | 更新频率 | | ------ | ---- | -------- | -------------------------------------------------------------------------------------------------- | ---- | | **裁剪** | LZ4 | \< 5 GiB | [下载](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4) | 每日 | | **归档** | ZSTD | \~300 GB | [下载](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst) | 每周 | #### 测试网 | 类型 | 压缩格式 | 大小 | URL | 更新频率 | | ------ | ---- | -------- | -------------------------------------------------------------------------------------------------- | ---- | | **裁剪** | LZ4 | \< 5 GiB | [下载](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4) | 每日 | | **归档** | ZSTD | \~800 GB | [下载](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst) | 每周 | ### 使用裁剪快照 裁剪快照包含最近的区块链状态(最后 100-1000 个区块)。 #### 第 1 步:设置环境变量 ```bash # Set service name (default: stable) export SERVICE_NAME=stable ``` #### 第 2 步:停止节点服务 ```bash # Stop the running node sudo systemctl stop ${SERVICE_NAME} # Verify it's stopped sudo systemctl status ${SERVICE_NAME} ``` #### 第 3 步:备份当前数据(可选) ```bash # Create backup directory mkdir -p ~/stable-backup # Backup current state (optional, requires significant space) cp -r ~/.stabled/data ~/stable-backup/ ``` #### 第 4 步:下载并解压裁剪快照 :::code-group ```bash [Mainnet] # Install dependencies sudo apt install -y wget zstd pv # Create snapshot directory mkdir -p ~/snapshot cd ~/snapshot # Download pruned snapshot with progress wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4 # Remove old data rm -rf ~/.stabled/data/* # Extract snapshot with progress indicator pv stable_pruned.tar.zst | zstd -d -c | tar -xf - -C ~/.stabled/ # Alternative extraction without pv zstd -d stable_pruned.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm stable_pruned.tar.zst ``` ```bash [Testnet] # Install dependencies sudo apt install -y wget lz4 pv # Create snapshot directory mkdir -p ~/snapshot cd ~/snapshot # Download pruned snapshot with progress wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 # Alternative: Download with resume support curl -C - -o snapshot.tar.lz4 https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4 # Remove old data rm -rf ~/.stabled/data/* # Extract snapshot with progress indicator pv snapshot.tar.lz4 | tar -I lz4 -xf - -C ~/.stabled/ # Alternative extraction without pv tar -I lz4 -xvf snapshot.tar.lz4 -C ~/.stabled/ # Clean up rm snapshot.tar.lz4 ``` ::: #### 第 5 步:重启节点 ```bash # Start the node sudo systemctl start ${SERVICE_NAME} # Check status sudo systemctl status ${SERVICE_NAME} # Monitor logs sudo journalctl -u stabled -f ``` ### 使用归档快照 归档快照包含完整的区块链历史。 #### 第 1 步:准备系统 ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Install dependencies sudo apt install -y wget zstd pv # Check available disk space (need 2x snapshot size) df -h ~/.stabled ``` #### 第 2 步:下载并解压归档快照 :::code-group ```bash [Mainnet] # Create working directory mkdir -p ~/snapshot cd ~/snapshot # Download archive snapshot wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst # Clear old data rm -rf ~/.stabled/data/* # Extract with high memory for better performance pv stable_archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/ # Alternative: Standard extraction zstd -d --long=31 stable_archive.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm stable_archive.tar.zst ``` ```bash [Testnet] # Create working directory mkdir -p ~/snapshot cd ~/snapshot # Download archive snapshot wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst # Clear old data rm -rf ~/.stabled/data/* # Extract with high memory for better performance pv archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/ # Alternative: Standard extraction zstd -d --long=31 archive.tar.zst -c | tar -xvf - -C ~/.stabled/ # Clean up rm archive.tar.zst ``` ::: #### 第 3 步:启动节点 ```bash # Start service sudo systemctl start ${SERVICE_NAME} # Verify sync status curl -s localhost:26657/status | jq '.result.sync_info' ``` ### 创建你自己的快照 #### 手动创建快照 ```bash # Stop node sudo systemctl stop ${SERVICE_NAME} # Create snapshot archive cd ~/.stabled tar -cf - data/ | lz4 -9 > ~/stable-snapshot-$(date +%Y%m%d).tar.lz4 # Create checksum sha256sum ~/stable-snapshot-*.tar.lz4 > checksums.txt # Restart node sudo systemctl start ${SERVICE_NAME} ``` #### 自动化快照脚本 ```bash #!/bin/bash # snapshot.sh - Automated snapshot creation # Configuration SNAPSHOT_DIR="/var/snapshots" STABLED_HOME="$HOME/.stabled" KEEP_DAYS=7 # Create snapshot directory mkdir -p $SNAPSHOT_DIR # Stop node sudo systemctl stop ${SERVICE_NAME} # Create snapshot SNAPSHOT_NAME="stable-snapshot-$(date +%Y%m%d-%H%M%S).tar.lz4" tar -cf - -C $STABLED_HOME data/ | lz4 -9 > $SNAPSHOT_DIR/$SNAPSHOT_NAME # Generate metadata cat > $SNAPSHOT_DIR/latest.json < { console.log("Unbonding completed for:", delegator); console.log("Amount:", ethers.formatEther(amount), "STABLE"); console.log("Tx:", event.log.transactionHash); }); console.log("Listening for UnbondingCompleted events..."); ``` ```bash npx tsx watchUnbonding.ts ``` ```text Listening for UnbondingCompleted events... Unbonding completed for: 0xabcd... Amount: 100.0 STABLE Tx: 0x12ab... ``` 关于完整的系统交易机制以及按用户筛选 / 历史查询的模式,请参阅[追踪解绑完成](/cn/how-to/track-unbonding)。 ### 各模块参考 每个预编译合约的完整方法列表、事件和授权规则都在其对应的参考页面中。 * [Bank 预编译合约](/cn/reference/bank-module-api):STABLE 代币转账和供应量查询。 * [Distribution 预编译合约](/cn/reference/distribution-module-api):奖励领取和佣金。 * [Staking 预编译合约](/cn/reference/staking-module-api):委托、解除委托、重新委托、验证人查询。 * [系统交易](/cn/reference/system-transactions-api):StableSystem 事件格式和授权。 ### 推荐的后续阅读 * [**追踪解绑完成**](/cn/how-to/track-unbonding) — 订阅通过 StableSystem 预编译合约发出的 UnbondingCompleted 事件。 * [**系统模块参考**](/cn/reference/system-modules-api-overview) — 直接查看各模块的 ABI、方法签名和事件结构。 * [**系统模块概念**](/cn/explanation/system-modules-overview) — 理解 Stable 为何通过预编译合约暴露 SDK 模块。 ## 验证智能合约 验证会将合约的源代码上传到区块浏览器,并证明它编译后与已部署的字节码一致。一旦完成验证,用户就可以在 Stablescan 上读取状态、调用函数并审计源代码,而无需重新托管你的代码。本指南将带你完成在 Stable 上验证由 Foundry 部署的合约。 ### 前置条件 * 一个已部署在 Stable 测试网或主网上的合约。如果你尚未部署,请参阅[部署智能合约](/cn/tutorial/smart-contract)。 * 已安装 Foundry(PATH 中可使用 `forge`)。 * 来自 `forge create` 输出的已部署合约地址。 ### 1. 确认已部署的地址 确保你拥有之前部署时的 `Deployed to` 地址。在[部署智能合约](/cn/tutorial/smart-contract)流程中,这是 `forge create` 之后打印出的值。 ```bash cast code 0xDeployedContractAddress --rpc-url https://rpc.testnet.stable.xyz | head -c 20 ``` ```text 0x6080604052600436... ``` 非空的字节码确认该合约已部署在该地址。 ### 2. 运行 forge verify-contract Foundry 的验证流程会将你的源代码提交给 Stablescan 验证器。 ```bash forge verify-contract \ 0xDeployedContractAddress \ src/Counter.sol:Counter \ --chain-id 2201 \ --verifier blockscout \ --verifier-url https://testnet.stablescan.xyz/api \ --watch ``` ```text Start verifying contract `0xDeployedContractAddress` deployed on 2201 Submitting verification of contract: Counter Submitted contract for verification: Response: `OK` GUID: `abc123...` URL: https://testnet.stablescan.xyz/address/0xDeployedContractAddress Contract verification status: Response: `OK` Details: `Pass - Verified` Contract successfully verified ``` `--watch` 会阻塞直到验证完成,这样你就无需轮询。在主网上,将 chain ID 换成 `988`,验证器 URL 换成 `https://stablescan.xyz/api`。 :::note **构造函数参数**:如果你的合约接受构造函数参数,请在命令中添加 `--constructor-args $(cast abi-encode "constructor(uint256,address)" 42 0xSomeAddress)`。如果没有这个标志,任何带有非空构造函数的合约都会验证失败。 ::: ### 3. 在 Stablescan 上确认验证 在浏览器中打开合约页面。 ```text https://testnet.stablescan.xyz/address/0xDeployedContractAddress ``` 此时 **Contract** 标签页应该会显示源代码、一个绿色的 "Verified" 徽章以及完整的 ABI。用户可以在 **Read Contract** 下读取状态,并在 **Write Contract** 下发送交易。 ### 故障排查 * **"Bytecode does not match"**:你的源代码编译出的字节码与已部署的不同。最常见的原因是 Solidity 版本或优化器设置不匹配。显式传入 `--compiler-version` 和 `--optimizer-runs` 以匹配你的 `foundry.toml`。 * **"GUID not found"**:验证器尚未注册你的提交。使用 `--watch` 重新运行,或手动检查响应中打印的 URL。 * **合约使用了库**:为每个链接的库添加 `--libraries src/Lib.sol:Lib:0xDeployedLibAddress`。 ### 推荐的后续步骤 * [**索引合约事件**](/cn/how-to/index-contract) — 使用 ethers.js 订阅链上事件并构建实时事件流。 * [**部署智能合约**](/cn/tutorial/smart-contract) — 搭建一个全新的 Foundry 项目并部署到 Stable 测试网。 * [**JSON-RPC 参考**](/cn/reference/json-rpc-api) — 查看 Stable 支持哪些 `eth_*` 方法用于链上交互。 ## 将 USDT0 作为 gas 使用 在 Stable 上,USDT0 既是链的原生资产,也是一种 ERC-20 代币。gas 代币是 USDT0,而不是单独的原生资产。只要调整三件事,标准的以太坊 gas 估算就能正常工作:`maxPriorityFeePerGas` 始终为 `0`、`baseFee` 以 USDT0 计价,以及原生转账中的 `value` 字段携带的是 USDT0(而非 ETH)。 本指南展示如何在 Stable 上正确构建交易,以及在迁移以太坊代码时需要改动什么。 ### 与以太坊相比的变化 | **字段** | **以太坊** | **Stable** | | :------------------------------- | :------- | :--------- | | Gas 代币 | ETH | USDT0 | | `maxPriorityFeePerGas` | 用于排序 | 忽略(设为 `0`) | | `baseFeePerGas` | 以 ETH 计价 | 以 USDT0 计价 | | `value`(原生转账) | 转移 ETH | 转移 USDT0 | | EIP-1559 交易格式 | 支持 | 支持 | | `eth_estimateGas`、`eth_gasPrice` | 支持 | 支持 | | `eth_maxPriorityFeePerGas` | 返回小费 | 返回 `0` | 由于交易格式没有变化,现有的 ethers.js、viem、Hardhat 和 Foundry 代码无需改动即可在 Stable 上运行。区别在于你如何*计算* gas 字段,而不是如何对它们进行编码。 ### 构建交易 获取 base fee,将 `maxPriorityFeePerGas` 设为 `0`,并将 base fee 翻倍作为安全余量。 ```typescript // sendNative.ts import { ethers } from "ethers"; import "dotenv/config"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); const block = await provider.getBlock("latest"); const baseFee = block!.baseFeePerGas!; const maxPriorityFeePerGas = 0n; // always 0 on Stable const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // 2x headroom const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: ethers.parseEther("0.001"), // 0.001 USDT0, 18 decimals maxFeePerGas, maxPriorityFeePerGas, }); const receipt = await tx.wait(1); console.log("Tx:", receipt!.hash); console.log("Gas used:", receipt!.gasUsed.toString()); console.log("Effective gas price:", receipt!.gasPrice.toString(), "(USDT0 wei-equivalent)"); ``` ```bash npx tsx sendNative.ts ``` ```text Tx: 0x8f3a...2d41 Gas used: 21000 Effective gas price: 1000000000 (USDT0 wei-equivalent) ``` 有效 gas 价格是一个以 USDT0 计价的值。在 `1 gwei` 时,一笔 21,000 gas 的原生转账大约花费 `0.000021` USDT0。 ### 以 USDT0 估算 gas 成本 `eth_estimateGas` 和 `eth_gasPrice` 的行为与以太坊完全相同。由于 USDT0 是 gas 代币,结果已经以 USDT0 计价。 ```typescript // estimate.ts import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz"); const gasPrice = await provider.send("eth_gasPrice", []); const gasEstimate = await provider.estimateGas({ to: "0xContractAddress", data: "0x...", }); const feeInUSDT0 = BigInt(gasPrice) * gasEstimate; console.log("Estimated fee:", ethers.formatEther(feeInUSDT0), "USDT0"); ``` ```bash npx tsx estimate.ts ``` ```text Estimated fee: 0.000021 USDT0 ``` :::warning `eth_maxPriorityFeePerGas` 在 Stable 上始终返回 `0`。如果你的钱包或 SDK 将 RPC 返回的优先费加在 base fee 之上,它仍然有效,但显示单独小费的费用界面将显示 `0`,应当隐藏。 ::: ### 工具配置 * **Hardhat / Foundry**:无需特殊配置。标准 EVM 设置即可工作。如果你的配置显式设置了优先费,请将其设为 `0`。 * **钱包**:隐藏或禁用优先小费的输入字段。显示它会产生误导,因为该值对排序或打包没有任何影响。 * **监控**:费用分析仪表板不应绘制优先费图表。它们在 Stable 上始终为零。 ### 从以太坊迁移时的常见错误 * **应用以 ETH 计价的小费**:从以太坊复制优先费常量并不会带来更快的打包。Stable 仅按 base fee 对交易排序。 * **将 `value` 当作 ETH**:原生转账的 `value` 是 USDT0。不要通过 ETH/USD 价格进行换算。 * **硬编码费用上限**:应根据实时的 `baseFeePerGas` 设置 `maxFeePerGas`(例如 `baseFee * 2`),而不是固定值,这样当 base fee 上升时交易才不会卡住。 ### 下一步推荐 * [**Gas 定价参考**](/cn/reference/gas-pricing-api) — 完整的 base-fee 模型、EIP-1559 格式以及 `eth_*` 方法行为。 * [**零 gas 交易**](/cn/how-to/zero-gas-transactions) — 让应用通过 Gas 豁免来承担 gas。 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 余额对账以及针对 USDT0 双重角色的合约设计。 ## 零 gas 交易 Gas Waiver 让应用程序可以代表用户承担 gas 费用。用户使用 `gasPrice = 0` 签署一笔交易,由治理注册的 waiver 将其包装,验证者以零成本为用户执行该调用。本指南将带你完成一笔符合条件的转账,展示如何验证 gas 已被豁免,并解释 waiver 涵盖与不涵盖的范围。 :::note **概念**:关于包装交易机制、授权模型和安全保证,请参阅 [Gas waiver](/cn/explanation/gas-waiver) 和 [Gas waiver 协议参考](/cn/reference/gas-waiver-api)。 ::: ### 你将构建的内容 一个两脚本流程,通过托管的 Waiver Server 提交一笔 USDT0 转账,获取收据,并确认 `gasPrice = 0`。 #### 演示 ```text step 1. Connect wallet, balance displayed as 0.01 USDT0 step 2. Send transaction via Gas Waiver → [Run] step 3. Result tx: 0x8f3a...2d41 Gas fee paid by you: 0.000000 USDT0 Balance after: 0.01 USDT0 ``` ### waiver 何时生效 当以下所有条件均满足时,交易符合条件: * 用户使用 `gasPrice = 0` 签署内部交易。 * 提交者是治理注册的 waiver 地址。 * 目标 `to` 地址和方法选择器位于 waiver 的 `AllowedTarget` 策略中。 * 包装交易发送到标记地址 `0x000000000000000000000000000000000000f333`,且 `value = 0` 和 `gasPrice = 0`。 如果其中任何一项不满足,验证者将拒绝该包装交易,不执行内部调用。未列入 `AllowedTarget` 的合约调用不在涵盖范围内。无法进行任意的自助 waiver;每个 waiver 都必须通过验证者治理注册。 ### 前置条件 * Waiver Server 的 API 密钥,由 Stable 团队签发。 * 已在 waiver 的 `AllowedTarget` 策略上注册的目标合约地址和方法选择器。 * 一个测试网上的用户钱包,无需 USDT0 用于支付 gas。 ### 步骤 1:签署符合条件的 InnerTx 用户使用 `gasPrice = 0` 签署一笔标准交易。在本示例中,该调用是一笔 USDT0 `transfer`,这是应用程序承担 gas 流程中常见的 `AllowedTarget`。 ```typescript // config.ts import { ethers } from "ethers"; import "dotenv/config"; export const CONFIG = { RPC_URL: "https://rpc.testnet.stable.xyz", CHAIN_ID: 2201, // 988 for mainnet WAIVER_SERVER: "https://waiver.testnet.stable.xyz", USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", }; export const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL); export const userWallet = new ethers.Wallet(process.env.USER_PRIVATE_KEY!, provider); ``` ```typescript // signInner.ts import { ethers } from "ethers"; import { CONFIG, provider, userWallet } from "./config"; const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [ "function transfer(address to, uint256 amount) returns (bool)" ], provider); const callData = usdt0.interface.encodeFunctionData("transfer", [ "0xRecipientAddress", ethers.parseUnits("0.001", 18), ]); const gasLimit = await provider.estimateGas({ from: userWallet.address, to: CONFIG.USDT0_ADDRESS, data: callData, }); const nonce = await provider.getTransactionCount(userWallet.address); const innerTx = { to: CONFIG.USDT0_ADDRESS, data: callData, value: 0, gasPrice: 0, gasLimit, nonce, chainId: CONFIG.CHAIN_ID, }; export const signedInnerTx = await userWallet.signTransaction(innerTx); console.log("Signed InnerTx:", signedInnerTx); ``` ```bash npx tsx signInner.ts ``` ```text Signed InnerTx: 0xf8a8...c1 ``` :::warning `gasPrice` 必须为 `0`。非零值会导致 waiver 服务器拒绝提交,验证者拒绝包装交易。 ::: ### 步骤 2:通过 Waiver Server 提交 Waiver Server 会包装已签署的内部交易并广播。你需要一个服务器签发的 API 密钥。 ```typescript // submit.ts import { CONFIG } from "./config"; import { signedInnerTx } from "./signInner"; const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.WAIVER_API_KEY}`, }, body: JSON.stringify({ transactions: [signedInnerTx] }), }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let txHash = ""; while (true) { const { done, value } = await reader.read(); if (done) break; for (const line of decoder.decode(value).trim().split("\n")) { const result = JSON.parse(line); if (result.success) { txHash = result.txHash; console.log(`tx confirmed: ${txHash}`); } else { console.error(`tx failed: ${result.error.message}`); } } } export { txHash }; ``` ```bash npx tsx submit.ts ``` ```text tx confirmed: 0x8f3a...2d41 ``` ### 步骤 3:验证收据显示零 gas 获取收据并确认 `effectiveGasPrice` 为 0。这就是用户未支付任何 gas 的密码学证明。 ```typescript // verify.ts import { provider } from "./config"; import { txHash } from "./submit"; const receipt = await provider.getTransactionReceipt(txHash); const gasUsed = receipt!.gasUsed; const effectiveGasPrice = receipt!.gasPrice; const totalFee = gasUsed * effectiveGasPrice; console.log("Gas used: ", gasUsed.toString()); console.log("Effective gas price:", effectiveGasPrice.toString()); console.log("Gas fee paid: ", `${totalFee.toString()} USDT0 (wei-equivalent)`); ``` ```bash npx tsx verify.ts ``` ```text Gas used: 21000 Effective gas price: 0 Gas fee paid: 0 USDT0 (wei-equivalent) ``` `effectiveGasPrice` 为 `0` 确认了该交易在已注册的 waiver 下执行,且用户未被收费。 ### Gas Waiver 不涵盖的范围 * **`AllowedTarget` 之外的合约**:任意合约调用不在涵盖范围内。每个目标都通过治理按 waiver 进行限定。 * **用户提交的包装交易**:如果用户直接提交到 `0x...f333`,将会失败。只有已注册的 waiver 地址才能进行包装。 * **费用提取**:验证者不接受内部交易或包装交易上任何非零的 `gasPrice`。 关于完整的策略模型和按 waiver 的范围规则,请参阅 [Gas waiver 协议](/cn/reference/gas-waiver-api)。 ### 推荐的后续步骤 * [**集成 Waiver Server**](/cn/how-to/integrate-gas-waiver) — 完整的 API 参考、批量提交、错误码和 NDJSON 流式传输。 * [**自托管 Gas Waiver**](/cn/how-to/self-hosted-gas-waiver) — 注册你自己的 waiver 地址,无需托管 API 即可广播包装交易。 * [**Gas waiver 协议**](/cn/reference/gas-waiver-api) — 阅读完整规范:标记路由、包装格式、治理控制。 ## 账户指南 账户标签下的每个指南、概念和参考,按你想要完成的任务进行分组。 ### 设置钱包 * [**创建钱包**](/cn/how-to/create-wallet) — 使用 ethers.js 或 Tether WDK 生成新的密钥对或从助记词恢复。 * [**代理钱包**](/cn/reference/agentic-wallets) — 用于 AI 代理的自托管钱包——它们与用户钱包的区别。 ### 委托账户(EIP-7702) * [**EIP-7702 概念**](/cn/explanation/eip-7702) — EIP-7702 在 Stable 上实现的功能及其安全模型。 * [**账户抽象操作指南**](/cn/how-to/account-abstraction) — 将 EIP-7702 应用于批量支付、消费限额和会话密钥。 ### 参考 * [**EIP-7702 API**](/cn/reference/eip-7702-api) — Type-4 交易格式和授权列表。 * [**订阅与收款**](/cn/how-to/subscribe-and-collect) — 将 EIP-7702 委托应用于订阅支付流程(交叉列出)。 ## Stable 上的账户 Stable 上的账户是标准的以太坊 EOA,可以通过 [EIP-7702 委托](/cn/explanation/eip-7702)选择性地执行智能合约逻辑。用户在钱包、批量支付、定期订阅和会话密钥中始终保持一个地址和一个私钥。代理使用相同的账户模型运行,无需任何托管中间件。 ### 你可以构建什么 * **钱包**:从助记词创建,支持原生 USDT0 余额查询和签名交易。 * **批量支付**:通过委托的 EOA 在一笔原子交易中执行多次转账。 * **支出限额**:通过委托逻辑在 EOA 本身上强制实施每笔交易或每日上限。 * **会话密钥**:向 dApp 授予一个范围受限、时间受限、预算受限的密钥,使用户无需为每个操作重新签名。 * **代理钱包**:用自托管密钥为 AI 代理注资,让其自主支付 x402 服务费用。有关提供商和集成模式,请参阅[代理钱包](/cn/reference/agentic-wallets)。 ### Stable 的不同之处 * **一个地址搞定一切。** 无需账户迁移即可解锁智能合约功能。EIP-7702 将代码委托*到*现有 EOA 上。 * **仅用 USDT0 支付 Gas。** 用户无需单独的原生代币。新账户用 USDT0 注资后即可立即交易。 * **多功能委托模式。** 单个委托可以组合批量、支出限额、会话密钥和订阅功能,因此一次委托即可覆盖你交付的所有功能。 ### 从这里开始 * [**创建钱包**](/cn/how-to/create-wallet) — 使用 ethers.js 或 Tether WDK 生成或恢复钱包。 * [**使用 EIP-7702 委托**](/cn/how-to/account-abstraction) — 将批量支付、支出限额和会话密钥应用于现有 EOA。 * [**Stable SDK**](/cn/explanation/sdk-overview) — 使用类型化客户端从任何账户签名并发送交易。 ### 接下来推荐 * [**账户指南索引**](/cn/explanation/accounts-guides) — 跳转到账户指南和参考的完整列表。 * [**EIP-7702 概念**](/cn/explanation/eip-7702) — 为什么委托无需账户迁移即可工作。 * [**订阅与收款**](/cn/how-to/subscribe-and-collect) — 将账户模型应用于定期支付流程。 ## 代理结算 代理结算是 Stable 面向机器支付的通道。代理持有 USDT0 余额,通过 HTTP 为某个资源付费,付款在同一请求周期内于链上结算。代理从同一个余额中同时支出付款和网络费用。无需独立的 gas 代币,无需注册,也无需轮换 API 密钥。 ### 这对代理为何重要 代理的交易方式与人类不同。它们持续运行,进行大量小额付款,且无法完成注册流程或轮换 API 密钥。Stable 上的结算正契合这种工作负载: * **USDT0 既是 gas 代币也是支付代币。** 代理钱包持有单一资产,并用它同时支付费用和付款。 * **亚美分、可预测的费用。** 费用以美元计价,因此代理可以为每个操作的成本做预算,而无需从波动的 gas 资产进行换算。 * **亚秒级最终确认。** 已付费的 HTTP 调用在请求生命周期内完成结算(约 700 毫秒出块时间),这使高频机器流量成为可能。 * **USDT 的分发覆盖。** USDT 是持有最广泛的稳定币;Stable 是专为它打造的场所。 ### 各层级如何协作 两个层级承担不同的职责,互为补充而非相互替代: * **x402** 是*支付标准*。它是一种 HTTP 原生协议,服务器以 `402 Payment Required` 响应,客户端签署授权,由 facilitator 提交。 * **MPP(Machine Payments Protocol)** 是 IETF 标准化路线上的标准,它通过更广泛的意图和多通道支持取代 x402;x402 是 Stable 目前支持的向后兼容子集。参见 [MPP](/cn/explanation/mpp)。 * **Stable** 是*结算层*。USDT0 的链上转账实际上在这里发生。 **facilitator** 位于两者之间:它验证已签名的付款并提交链上调用,从而使开发者无需运行结算基础设施。请参见 [Facilitators](/cn/reference/agentic-facilitators),了解目前支持 Stable 的提供商。 ```text agent (client) ──HTTP──▶ resource server ──signed payment──▶ facilitator ──tx──▶ Stable (returns 402) (verify + submit) (USDT0 settles) ``` ### 你可以构建什么 * **按调用付费的 API**,以 USDT0 按请求计价,通过 x402 或 MPP 结算。 * **代理对代理的商业**,一个代理通过 HTTP 为另一个代理的服务付费。 * **付费的 MCP 工具**,封装 x402 端点,使 AI 客户端可以通过提示词调用并为其付费。 * **自主采购**,基于已编入预算的 USDT0 余额。 * **基于用量的计费**,按每次请求结算而非按发票结算。 * **代理钱包**,仅以 USDT0 充值,无需托管中间件。 ### 从这里开始 * [**构建按调用付费的 API**](/cn/how-to/build-pay-per-call) — 搭建一个 x402 门控的端点,并在请求中结算一笔真实的 USDT0 付款。 * [**在 Stable 上构建 MPP 端点**](/cn/how-to/build-mpp-endpoint) — 为 USDT0 编写三个 MPP 自定义方法钩子,并在 Stable 上结算。 * [**使用 AI 进行开发**](/cn/how-to/develop-with-ai) — 将 Docs MCP 和 Runtime MCP 接入你的 AI 编辑器,并粘贴 Stable 上下文块。 * [**通过 MCP 服务器付费**](/cn/how-to/pay-with-mcp) — 将 x402 付费 API 暴露为 MCP 工具,使代理可以通过自然语言提示词调用它们。 ### 推荐的后续阅读 * [**深入了解 x402**](/cn/explanation/x402) — 阅读 HTTP 支付协议在 Stable 上端到端的工作方式。 * [**MPP**](/cn/explanation/mpp) — x402 所属的更广泛的 IETF 标准化路线标准。 * [**Facilitators**](/cn/reference/agentic-facilitators) — 查看哪些 facilitator 已在 Stable 上结算 USDT0 付款。 ## AI 与智能体指南 AI/智能体标签页下的所有指南、概念和参考,按你想要完成的任务分组。 ### 配置 AI 编辑器 * [**使用 AI 进行开发**](/cn/how-to/develop-with-ai) — 安装 Docs MCP、Runtime MCP、智能体技能,并粘贴 Stable 上下文块。 * [**创建智能体钱包**](/cn/how-to/create-wallet) — 通过 WDK 自托管密钥——智能体支付的基础。 ### 服务变现与使用 * [**构建按调用付费的 API**](/cn/how-to/build-pay-per-call) — 使用 x402 中间件,为任意 HTTP 端点按请求以 USDT0 定价。 * [**通过 MCP 服务器付费**](/cn/how-to/pay-with-mcp) — 将 x402 付费 API 封装为 MCP 工具,使 AI 客户端可以调用并为其付费。 ### 参考 * [**智能体结算服务**](/cn/reference/agentic-facilitators) — Stable 上用于智能体间商务的结算服务。 * [**智能体钱包**](/cn/reference/agentic-wallets) — 用于自主智能体的钱包规范。 ### 基础概念 * [**x402(HTTP 原生支付)**](/cn/explanation/x402) — 智能体用于按请求付费的 HTTP 协议。 * [**MPP**](/cn/explanation/mpp) — x402 所属的更广泛的 IETF 标准,支持会话和多通道。 * [**ERC-3009**](/cn/explanation/erc-3009) — x402 通过其进行结算的签名授权标准。 ## Autobahn ### BFT 协议的权衡:低延迟 vs. 高鲁棒性 现代拜占庭容错(BFT)共识协议通常运行在部分同步模型下,该模型假设在某个不确定的时间点后,网络最终会变得稳定,消息传递延迟会有上限。虽然这个模型在协议设计上是实用的,但现实部署中很少能享受长时间的持续稳定。相反,系统经常经历同步阶段后出现短暂中断,如延迟激增、节点宕机或恶意攻击。这些短暂中断被称为 **“突变(blips)”**。 在这种情况下,现有的共识协议通常被迫在 **网络稳定时的低延迟** 与 **故障状态下的高鲁棒性** 之间做出取舍: * **传统的基于视图(view-based)的 BFT 协议**(如 PBFT 和 HotStuff)在网络良好时响应迅速,但一旦发生突变,其性能会迅速恶化。这种性能衰退称为“宿醉效应(hangover)”,即使网络恢复后仍会持续,因为积压的请求会延迟后续交易处理。 * **基于 DAG 的 BFT 协议**(如 [Narwhal & Tusk](https://arxiv.org/pdf/2105.11827)/[Bullshark](https://arxiv.org/pdf/2201.05677))将数据传播(DAG)与共识(BFT)解耦,允许交易异步传播至各副本。这种设计支持高吞吐量,并可在网络中断时继续推进。然而,这些协议即使在网络良好时也会因异步排序机制的复杂性而导致高延迟。 [**Autobahn**](https://arxiv.org/pdf/2401.10369) 引入了一种全新方法,融合了这两种设计理念的优势。它结合了 DAG 协议的高吞吐与对突变的容忍性,以及传统共识协议的低延迟性能。Autobahn 的核心是一个高度并行的数据传播层,无论共识进展如何,始终以网络速度传播提案。在此之上,Autobahn 运行一个低延迟的部分同步共识协议,通过引用数据层的轻量级快照来提交提案。 Autobahn 的一个显著特点是其在突变后无性能退化的恢复能力,称为 **“无缝性(seamlessness)”**。这意味着系统在网络稳定后可立即恢复全速和低延迟运行,无需对积压交易进行代价高昂的重复处理。通过将数据可用性与排序清晰分离,并避免协议自身引起的同步延迟,Autobahn 为真实环境下的区块链共识提供了一个既有高鲁棒性又响应迅速的基础。 ### Autobahn 架构概览 Autobahn 的架构围绕其两大核心层:**数据传播层** 和 **共识层**,职责分离明确。这种解耦借鉴了 Narwhal 等 DAG 系统的设计,但 Autobahn 对其进行了增强,以支持无缝性和更低延迟。 数据传播层负责以可扩展的异步方式广播客户端交易。每个副本维护一条独立的交易批处理通道,称为“车道(lane)”,这些车道可独立于共识状态进行传播和认证。即使共识过程暂停,车道仍持续增长,保证系统对客户端始终保持响应。 共识层运行一个基于 PBFT 风格的部分同步协议。但与传统共识需就每一批交易达成一致不同,Autobahn 共识协议的处理方式类似“车头快照(tip cut)”,即各条车道当前状态的精简摘要。这种设计允许 Autobahn 一次性提交任意规模的数据,减少突变带来的影响。 相较于 HotStuff(数据与共识紧耦合,领导者失败时易停滞)和 Bullshark(因 DAG 遍历和同步造成高延迟),Autobahn 提供了更平滑、更快速的共识体验。它继承了 DAG 的并行性,却规避了其延迟劣势。 ### **数据传播层:车道与车辆** ![Autobahn:无缝高速 BFT](/images/autobahn-high-speed1.png) *Autobahn:无缝高速 BFT* 在 Autobahn 中,每个副本在其独立推进的链中提出交易,称为一条 **车道(lane)**。每个数据提案都包含来自其他副本的确认集合,称为 **“车辆(car)”**(即可用请求认证,Certification of Available Request)。这些车辆作为数据可用性的证明(PoA),确保至少一个诚实副本持有数据,并在需要时可重新传输。 车辆通过在每个新提案中引用前一个车辆构建出链式结构。这种结构保证了只需验证车道的车头即可确认整个车道历史数据的可用性。这种可传递的可用性证明使 Autobahn 能即时引用车头快照,避免了 DAG 遍历操作或额外的同步操作。 与传统 DAG 协议不同,Autobahn 避免了强制全局可用性和防双重广播所需的高成本广播步骤。它使用尽可能少的操作,信任每个 PoA 至少有一个诚实数据副本,从而即使在负载变化或部分故障下也能实现高吞吐与低延迟。数据层独立于共识持续推进,确保在突变期间仍保持响应。 ### **共识层:低延迟达成一致** ![Autobahn:无缝高速 BFT](/images/autobahn-high-speed2.png) *Autobahn:无缝高速 BFT* Autobahn 的共识层在经典 PBFT 原则基础上引入关键优化,降低延迟并支持无缝恢复。每个共识时隙的目标是提交一个 **“车头快照(tip cut)”**,该快照捕捉来自各副本车道的最新认证提案。共识领导者使用两阶段提交流程(Prepare 和 Confirm)提出该快照。 在 Prepare 阶段,副本对提议的快照进行投票。如果领导者迅速收到足够投票(完整法定数),可进入快速路径(Fast Path),仅需 3 次消息延迟即可完成提交。否则,它将进入 Confirm 阶段,收集第二轮投票后在 6 次消息延迟内完成提交。 一大创新在于将数据同步从共识投票中拆解出来。副本可仅依据认证车头进行投票,即便尚未接收完整提案数据。这是安全的,因为 PoA 保证了数据的可检索性。同步操作并行进行,并在执行阶段前完成,避免协议阻塞。如遇领导者故障或超时,系统通过超时证书触发视图变更,由新领导者继续推进。 ### Autobahn 的核心特性 Autobahn 满足 BFT 协议标准的 **安全性** 与 **活性** 保证。安全性确保不会有两个正确副本在同一时隙提交不同区块;活性确保在全局稳定时间(GST)后,若最终选出正确领导者,则系统必将前进。 更重要的是,Autobahn 实现了真正的 **无缝性(seamlessness)**。其共识层可在常数时间(Constant Time)内提交任意规模的历史数据,避免因协议机制而导致的“宿醉”。即便经历突变,只要同步恢复,所有已传播的提案可立即被提交。这种能力使 Autobahn 能在间歇性故障环境中流畅运行,其恢复时间与响应速度均优于传统 BFT 协议。 此外,该协议还具备 **横向扩展性**。每个副本通过其车道为系统贡献吞吐量,随着参与者增加,快照提交容量自然增长,使 Autobahn 非常适合需要高性能与高鲁棒性的规模化部署。 ### **低延迟遇上高鲁棒性** Autobahn 在理想和故障注入的测试条件下与主流 BFT 协议(尤其是 Bullshark 和 HotStuff)进行对比测试。结果表明,Autobahn 兼具两者之长:在处理能力上匹敌 Bullshark(每秒处理超过 23 万笔交易),而延迟则降低了 50% 以上。 在网络良好条件下,Autobahn 仅需 3 至 6 次消息延迟即可提交交易,而 Bullshark 需 12 次,这使其实际提交延迟低至 280 毫秒,而 Bullshark 超过 590 毫秒。不同于 HotStuff 在突变后因积压处理而产生的长时间宿醉,Autobahn 能在网络稳定后一次性提交全部积压内容。 在领导者失效或部分网络分叉(partial network partitions)等故障场景下,Autobahn 表现出无缝恢复能力。在故障期间持续传播数据,并在共识恢复后迅速提交已积累提案。这些性能优势使 Autobahn 成为区块链平台在追求低延迟响应与高吞吐容错性方面的理想选择。 ### 延伸阅读 欲了解更多技术细节,请参考: * [Autobahn: Seamless high speed BFT](https://arxiv.org/pdf/2401.10369) ## 银行模块 ### 概述 Stable SDK 中的 `x/bank` 模块仅提供基本的代币管理功能。 每个代币都可以无限制地转移到任何账户,用户无法委托其他账户代为转移其代币到其他账户。 因此,`bank` 预编译合约在 Stable SDK 现有的 `x/bank` 模块基础上提供了额外的授权和委托功能。 ### 目录 1. **[概念](#concepts)** 2. **[配置](#configuration)** 3. **[方法](#methods)** 4. **[事件](#events)** ### 概念 该预编译合约提供 ERC20 标准方法 - 如用于转账的 `transfer` 和 `balanceOf`,以及用于委托的 `transferFrom`、`approve` 和 `allowance`。这些方法可以直接调用,无需注册合约地址。 然而,`mint` 和 `burn` 方法需要合约地址被列入白名单,通过 `x/precompile` 模块注册。 ```go func (p *Precompile) mint( ctx sdk.Context, contract *vm.Contract, denom string, method *abi.Method, stateDB vm.StateDB, args []interface{}, ) ([]byte, error) { // ... // mint method is only allowed for the registered caller contract if _, err := precompilecommon.CheckPermissions(ctx, p.precompileKeeper, contract.CallerAddress, CallerPermissions); err != nil { return nil, err } ``` 额外的验证过程可以保证调用此预编译合约的代币合约是经过授权的。 需要通过治理提案来注册代币合约地址及其denomination到 `x/precompile` 模块的白名单中。 ### 配置 合约地址和gas费用已预定义。 #### 合约地址 * `0x0000000000000000000000000000000000001003` STABLE (用于治理代币) ### 方法 #### `mint` 铸造请求数量的新代币并转移到账户。 要铸造的代币数量必须大于零。 当代币成功铸造并转移到账户时,会发出 `PrecompiledBankMint` 事件。 注意: * 禁止铸造治理代币。 * 调用铸造方法的合约必须在 x/precompile 模块中注册。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------ | ------- | --------- | | to | address | 接收铸造代币的地址 | | amount | uint256 | 要铸造的代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------------- | | success | bool | 如果代币成功铸造并转移到账户,则为true | #### `burn` 从账户中销毁请求数量的代币。 要销毁的代币数量必须大于零。 当代币成功销毁时,会发出 `PrecompiledBankBurn` 事件。 注意: * 禁止销毁治理代币。 * 调用铸造方法的合约必须在 x/precompile 模块中注册。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | from | address | 销毁代币的地址 | | amount | uint256 | 要销毁的代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果代币成功销毁,则为true | #### `transfer` 将请求数量的代币从发送者转移给接收者。 代币必须设置为可发送。要转移的代币数量必须大于零。 当代币成功转移时,会发出 `PrecompiledBankTransfer` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | to | address | 接收代币的地址 | | amount | uint256 | 要转移的代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果代币成功转移,则为true | #### `transferFrom` 由授权的支出者在允许额度范围内将请求数量的代币从所有者转移给接收者。 代币必须设置为可发送。 要转移的代币数量必须大于零并且小于或等于当前允许额度。 当代币成功转移时,会发出 `PrecompiledBankTransfer` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------ | ------- | -------- | | from | address | 转出代币的地址 | | to | address | 接收代币的地址 | | amount | uint256 | 要转移的代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果代币成功转移,则为true | #### `multiTransfer` 将代币从单个账户转移到多个账户。 代币必须设置为可发送。 转移给每个接收者的代币数量必须大于零。 当代币成功转移时,每个接收者都会发出 `PrecompiledBankTransfer` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------ | ---------- | ------------- | | to | address\[] | 接收转移代币的地址数组 | | amount | uint256\[] | 转移给每个接收者的代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------------- | | success | bool | 如果代币成功转移给每个接收者,则为true | #### `approve` 授权支出者从所有者账户转移代币。 要授权的代币数量必须大于零。 当授权成功设置时,会发出 `PrecompiledBankApproval` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------- | ------- | -------- | | spender | address | 要授权的地址 | | value | uint256 | 要授权的代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果授权成功设置,则为true | #### `revoke` 撤销支出者从所有者转移代币的授权。 当授权成功撤销时,会发出 `PrecompiledBankRevoke` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------- | ------- | ------ | | spender | address | 要撤销的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果授权成功撤销,则为true | #### `balanceOf` 返回账户的代币余额。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------- | ------- | ---------- | | account | address | 要获取代币余额的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ------- | -------- | | balance | uint256 | 账户中的代币数量 | #### `totalSupply` 返回代币的总供应量。 ##### 输入参数 无 ##### 输出参数 | 名称 | 类型 | 描述 | | ----------- | ------- | ------ | | totalSupply | uint256 | 代币的总数量 | #### `allowance` 返回支出者仍可从所有者提取的金额。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------- | ------- | ------ | | owner | address | 所有者的地址 | | spender | address | 支出者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------ | ------- | ------- | | amount | uint256 | 授权的代币数量 | ### 事件 所有从此预编译合约发出的事件都以 `PrecompiledBank` 为前缀。 为了避免歧义,调用此预编译合约的代币合约应避免使用相同前缀的事件名称。 #### PrecompiledBankMint | 名称 | 类型 | 索引 | 描述 | | ------ | ------- | -- | --------- | | from | address | Y | 铸造代币的地址 | | to | address | Y | 接收铸造代币的地址 | | amount | uint256 | N | 铸造的代币数量 | #### PrecompiledBankBurn | 名称 | 类型 | 索引 | 描述 | | ------ | ------- | -- | ------- | | from | address | Y | 销毁代币的地址 | | to | address | Y | 此方法中未使用 | | amount | uint256 | N | 销毁的代币数量 | #### PrecompiledBankTransfer | 名称 | 类型 | 索引 | 描述 | | ------ | ------- | -- | --------- | | from | address | Y | 转移代币的地址 | | to | address | Y | 接收转移代币的地址 | | amount | uint256 | N | 转移的代币数量 | #### PrecompiledBankApproval | 名称 | 类型 | 索引 | 描述 | | ------- | ------- | -- | ------- | | owner | address | Y | 授权代币的地址 | | spender | address | Y | 被授权的地址 | | value | uint256 | N | 授权的代币数量 | #### PrecompiledBankRevoke | 名称 | 类型 | 索引 | 描述 | | ------- | ------- | -- | --------- | | owner | address | Y | 撤销代币授权的地址 | | spender | address | Y | 被撤销的地址 | | value | uint256 | N | 授权的代币数量 | ## 桥接安全性与 DVN LayerZero 桥的安全性,完全取决于确认某条链上发送的消息确实在另一条链上发生的验证层。这个层就是去中心化验证者网络(Decentralized Verifier Network,DVN)。本页将解释 DVN 的作用、Stable 如何在其桥上配置 DVN,以及为什么任何单个 DVN 的泄露都不会使 Stable 面临风险。 ### DVN 如何工作 当 LayerZero 消息从链 A 移动到链 B 时,目标合约不会立即执行它,直到一组配置好的 DVN 独立证明该消息是真实的。每个应用都选择自己的配置: * **必需 DVN(Required DVNs)。** 每个必需 DVN 都必须签名后,消息才会被接受。 * **带 N-of-M 阈值的可选 DVN(Optional DVNs)。** 可在必需集合之上额外添加一个可选池,设置类似 2-of-5 的阈值,在必需签名之外还必须满足该阈值。 * **区块确认深度。** DVN 在签名前等待的源链确认数量。 桥的安全性完全由这个配置决定。如果采用 1/1 配置,仅由单个 DVN 作为唯一验证者,那么该 DVN 签名密钥的任何泄露都会让攻击者伪造跨链消息。而横跨三个独立运营商的 3/3 配置,则要求三者同时被攻破。这其中的差别,就是因单个被盗密钥而失去整座桥,与在针对某一运营商的定向攻击中幸存下来之间的差别。 ### Stable 的配置 Stable 的桥运行 **3/3 必需 DVN** 配置,由三个独立运营商组成:**LayerZero Labs**、**Canary** 和 **Horizen**。三者都必须对每条跨链消息签名后,目标合约才会执行它。这里没有带阈值的可选池;必需集合就是全部的验证面。 单个签名密钥被泄露(包括 LayerZero 自己的密钥)对这种安全态势毫无作用。伪造一条消息将需要同时攻破全部三个独立运营商。 关于 DVN 合约地址,请参阅 [桥:Stable 的 DVN 运营商](/cn/reference/bridges#stable-s-dvn-operators)。 ### STABLE OFT 架构 STABLE 代币使用 LayerZero 的全链同质化代币(Omnichain Fungible Token,OFT)标准桥接到其他链。部署了两种合约类型: * **`StableOFTAdapter`** 部署在 Stable 上。当 STABLE 被跨链发送时,该适配器在主链上锁定 STABLE 并发出一条 LayerZero 消息。 * **`StableOFTUpgradeable`** 部署在每条远程链上。当消息被配置的 DVN 验证后,该合约在目标链上铸造 STABLE,并在返回路径上销毁它,从而使主链供应量保持权威性。 关于各链上的已部署地址,请参阅 [桥:STABLE OFT 合约](/cn/reference/bridges#stable-oft-contracts)。 ### 运营依赖 Stable 自身的桥接安全性独立于上游协议,但当合作协议暂停其自身的桥时,通过 Stable 的跨链流动仍可能暂停。例如,当 USDT0 暂停跨链铸造和销毁时,USDT0 无法进出 Stable,直到 USDT0 恢复为止。Stable 内的资金仍可自由流动;仅特定的跨链操作不可用。 通过合作桥进行路由的应用界面应清楚地传达这一点,以便用户理解其中的区别:他们的资金没有风险,只是某条特定的跨链路径暂时不可用。 ### 下一步推荐 * [**将 USDT0 桥接到 Stable**](/cn/explanation/usdt0-bridging) — 了解 USDT0 如何通过 OFT Mesh 和 Legacy Mesh 到达 Stable。 * [**桥提供商与地址**](/cn/reference/bridges) — 参考合约地址、DVN 运营商和支持的桥提供商。 * [**LayerZero DVN 文档**](https://docs.layerzero.network/v2/concepts/protocol/security-stack-dvns) — 阅读 LayerZero 关于必需和可选 DVN 验证的规范。 ## 概览 初次接触 Stable?请先运行[快速开始](/cn/tutorial/quick-start)。只需五分钟即可发送一笔测试网交易,让本栏其余内容有可对接的基础。 ### 探索 * [**账户**](/cn/explanation/accounts-overview) — 创建钱包、使用 EIP-7702 委托 EOA,并为用户和智能体限定会话密钥范围。 * [**支付**](/cn/explanation/payments-overview) — 发送 USDT0、构建 P2P 和订阅流程、使用 ERC-3009 结算发票,并通过 x402 为 API 定价。 * [**合约**](/cn/explanation/contracts-overview) — 部署、验证和索引 Solidity 合约,并调用 Bank / Distribution / Staking 预编译合约。 * [**AI 与智能体**](/cn/explanation/agent-settlement) — 将 MCP 服务器接入 AI 客户端,并暴露智能体可通过提示词调用的 x402 付费工具。 ### 从这里开始 * [**快速开始**](/cn/tutorial/quick-start) — 连接测试网、为钱包充值,并发送你的第一笔 USDT0 交易。 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 在同一余额上进行原生和 ERC-20 转账,附带 TypeScript 示例。 * [**部署智能合约**](/cn/tutorial/smart-contract) — 搭建 Foundry、配置 Stable,并部署 Counter 合约。 * [**Stable SDK**](/cn/explanation/sdk-overview) — 使用类型化的 TypeScript 客户端在 Stable 上进行转账、跨链桥接和兑换。 ## 隐私转账(Confidential Transfer 随着区块链在企业中的应用不断加速,尤其是在稳定币领域,**对交易隐私的需求日益增长**。许多企业在处理财务操作时需要保持隐私性,以保护如付款金额等敏感信息。为满足这一需求,Stable 正在开发**隐私转账(Confidential Transfer)功能**,以在隐私保护与监管合规之间实现平衡。 Stable 利用零知识加密(Zero-Knowledge Cryptography, 简称 ZK)技术,构建了一个**隐私转账层(Confidential Transfer Layer)**,使交易双方可以在不公开链上交易金额的前提下完成代币转账。在此机制下: * **交易金额将被加密隐藏**,而非直接记录在链上。 * **发送方和接收方地址依然公开可见**,以便于遵守金融监管和审计要求。 这种设计保证了:**交易金额仅对交易参与方和授权的监管审计方可见**,在保护用户隐私的同时,也不牺牲法律合规性。 在当前业界的实现中,Solana 的 Confidential Transfer 模型被认为是该功能的有力候选方案之一。然而,Stable 仍在积极探索其他架构,力求找到最具可行性和可扩展性的解决方案。 最终选定的方案将必须契合 Stable 的核心使命:**为企业提供安全、合规且具备企业级可靠性的区块链基础设施**。 ## 共识 ### 基于 StableBFT 的 PoS 共识 Stable Blockchain 采用 **StableBFT**,这是一种基于 CometBFT 构建的定制化 PoS 共识协议,能够实现高吞吐量、低延迟和强可靠性。StableBFT 提供确定性最终性(区块一经纳入即为最终状态,不存在分叉),并具备拜占庭容错能力,可容忍多达 1/3 的验证者发生故障或恶意行为。 为进一步优化共识性能,Stable 计划在不久的将来实施以下改进: * **解耦交易与共识传播**:将交易传播层与共识传播层分离,可以防止交易侧的网络拥塞干扰共识通信。 * **将交易直接广播给区块提议者**:在当前模型中,交易通过节点之间的点对点传播进行扩散,从而在整个网络中产生大量传播流量。Stable 旨在通过使交易直接广播给区块提议者来提升效率。 ### 未来路线图:基于 DAG 的共识 为显著加速共识,Stable 计划将其协议升级为基于 DAG 的设计,可实现高达 5 倍的速度提升。 像 PBFT 和 HotStuff 这样的传统基于视图(view-based)的 BFT 协议,在稳定的网络条件下针对低延迟进行了优化。然而,它们在出现中断时性能会显著下降,并且在临时故障后往往会经历较长的恢复延迟。 像 Narwhal 和 Tusk 这样的第一代基于 DAG 的引擎表明,将数据传播与共识排序解耦可以消除单一提议者瓶颈,并提升网络不稳定状态下的健壮性。然而,它们的架构与 CometBFT 等系统并不直接兼容,因为它们偏离了传统的基于高度(height-based)的区块语义和内存池设计。 [Autobahn](/cn/explanation/autobahn) 提供了一种 PBFT-on-DAG 架构,能够更自然地与 Stable 的共识层集成,同时在正常条件下提供低延迟,并在面对网络故障时实现快速恢复。Stable 团队与 Autobahn 论文的作者保持着密切的合作关系,并将利用 Autobahn 的架构来最大化 StableBFT 的性能。 构建于 Autobahn 之上的 StableBFT 将实现: * 通过消除单一领导者限制,实现并行提议处理。 * 通过将数据传播与最终排序分离,实现更快的最终性。 * 通过强健的 BFT 机制,增强应对网络异常的韧性。 这种先进的共识设计基于内部概念验证支持更高的吞吐量,该验证已在受控环境中展示出超过 200,000 TPS(仅共识)的性能。 ### 下一步推荐 * [**Autobahn**](/cn/explanation/autobahn) — 阅读支撑 StableBFT 基于 DAG 升级路径的协议论文。 * [**执行**](/cn/explanation/execution) — 了解区块如何从共识进入并行执行。 * [**最终性**](/cn/explanation/finality) — 在基于 RPC 进行构建时应用 Stable 的单槽最终性。 ## 合约指南 合约选项卡下的每个指南、概念和参考,按你想要完成的目标分组。 ### 构建并发布合约 * [**部署**](/cn/tutorial/smart-contract) — 搭建 Foundry 项目并将 Counter 部署到 Stable 测试网。 * [**验证**](/cn/how-to/verify-contract) — 将源代码上传到 Stablescan,让用户能够阅读和调用你的合约。 * [**索引事件**](/cn/how-to/index-contract) — 使用 ethers.js 构建实时事件流,并进行历史数据回填。 ### 调用系统模块 * [**使用系统模块**](/cn/how-to/use-system-modules) — 从 Solidity 或 ethers.js 调用 Bank、Distribution 和 Staking 预编译合约。 * [**追踪解绑完成**](/cn/how-to/track-unbonding) — 订阅通过 StableSystem 预编译合约发出的 UnbondingCompleted 事件。 ### 参考 * [**系统模块参考**](/cn/reference/system-modules-api-overview) — 预编译合约地址以及各模块的 ABI 指针。 * [**JSON-RPC API**](/cn/reference/json-rpc-api) — 支持的 `eth_*`、`net_*`、`web3_*` 和 `debug_*` 方法。 ### 基础概念 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 双重角色余额、对账事件以及合约设计规则。 * [**与 Ethereum 的差异**](/cn/explanation/ethereum-comparison) — Gas 代币、最终性、优先级小费以及 EVM 兼容性。 ## Stable 上的合约 Stable 完全兼容 EVM。Solidity、Vyper、Hardhat、Foundry、ethers.js 和 viem 都无需修改即可使用。只要将工具指向 Stable 的 RPC,现有合约即可原样部署。在标准 EVM 之上,Stable 将协议级模块(Bank、Distribution、Staking)以预编译合约的形式暴露在固定地址上,因此你的 Solidity 可以直接调用质押和奖励分发功能,而无需重新实现它们。 ### 你可以构建什么 * **标准应用合约**(ERC-20、ERC-721、托管、AMM),使用任意 EVM 工具链。 * **经过验证、已索引的合约**,在 Stablescan 上通过 ethers.js 提供实时事件流。 * **协议集成合约**,可从 Solidity 调用 Bank / Distribution / Staking 预编译合约。 * **系统交易监听器**,通过标准的 `eth_getLogs` 监视协议发出的事件(例如解绑完成)。 ### Stable 的不同之处 * **USDT0 是 gas 代币。** `maxPriorityFeePerGas` 必须为 `0`。原生转账中的 `value` 字段携带的是 USDT0,而不是 ETH。参见[使用 USDT0 作为 gas](/cn/how-to/work-with-usdt-gas)。 * **USDT0 具有双重角色。** 持有原生 USDT0 的合约,其余额可能因 ERC-20 的 `transferFrom` 或 `permit` 而发生变化——切勿在 `uint256` 中镜像原生余额。参见 [Stable 上的 USDT0 行为](/cn/explanation/usdt0-behavior)。 * **预编译地址在测试网和主网中是固定的。** 将它们作为常量固化到你的合约中。 ### 从这里开始 * [**部署**](/cn/tutorial/smart-contract) — 搭建 Foundry,配置 Stable,并部署 Counter。 * [**验证**](/cn/how-to/verify-contract) — 使用 forge verify-contract 将源码上传到 Stablescan。 * [**索引**](/cn/how-to/index-contract) — 使用 ethers.js 订阅事件并回填历史日志。 ### 推荐下一步 * [**合约指南索引**](/cn/explanation/contracts-guides) — 合约指南、预编译参考和系统模块 ABI 的完整列表。 * [**使用系统模块**](/cn/how-to/use-system-modules) — 从 Solidity 和 ethers.js 调用 Bank / Distribution / Staking。 * [**JSON-RPC 参考**](/cn/reference/json-rpc-api) — Stable 支持哪些 `eth_*` 和 `debug_*` 方法。 ## 核心概念 掌握四个概念就足以开始构建。每个小节都会定义概念、展示示例,并链接到完整参考。 ### USDT0 作为 gas 你使用 USDT0 支付交易费用,这正是你已经持有并用于交易的同一种资产。无需为第二种代币充值或管理。 USDT0 既是原生 gas 资产(18 位小数,通过 `address(x).balance` 读取),也是 ERC-20 代币(6 位小数,通过 `USDT0.balanceOf(x)` 读取)。两种接口操作的是同一个底层余额,协议会自动协调 12 位的精度差异。 ```solidity // Both read the same balance: uint256 native = address(user).balance; // 18 decimals uint256 erc20 = IERC20(USDT0).balanceOf(user); // 6 decimals ``` :::warning 余额协调会在储备地址 `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370` 处发出额外的 `Transfer` 事件。回放 `Transfer` 事件的索引器必须过滤掉与该地址之间的转账,否则会在无声中重复计算余额。 ::: 阅读更多:[USDT0 作为 gas](/cn/explanation/usdt-as-gas-token) · [USDT0 在 Stable 上的行为](/cn/explanation/usdt0-behavior)。 ### 有保障的区块空间 Stable 为预先分配的企业级工作负载保留每个区块的一部分容量。即使在普通流量拥堵时,被保留的流量仍以可预测的延迟和成本结算;它不参与费用市场的竞争。 这种行为在调用方层面是透明的。你以常规方式提交交易;分配在协议层面对已注册的账户生效。 阅读更多:[有保障的区块空间](/cn/explanation/guaranteed-blockspace)。 ### USDT 转账聚合器 大批量的 USDT0 转账会通过受 MapReduce 启发的流水线进行批处理和并行验证。单个账户的失败会被隔离,因此一笔坏的转账不会中止整个批次。 调用方一侧的转账 API 保持不变。你以常规方式提交转账,无需更改代码即可获得吞吐量提升。 阅读更多:[USDT 转账聚合器](/cn/explanation/usdt-transfer-aggregator)。 ### EVM 兼容性 标准 EVM 工具无需更改即可使用。在 EVM 层面,有三种行为与以太坊不同(USDT0 作为 gas,已在上文介绍,是第四种)。 **单槽位最终性。** 交易一旦被包含进区块即为最终确认。区块大约每 0.7 秒产出一次。 **无优先级小费。** `maxPriorityFeePerGas` 始终被忽略。实际的 gas 价格是协议设定的基础费用。 ```typescript import { ethers } from "ethers"; const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const tx = await wallet.sendTransaction({ to: "0xRecipientAddress", value: ethers.parseEther("0.1"), maxFeePerGas: baseFee * 2n, // 2x base fee as safety margin maxPriorityFeePerGas: 0n, // always 0 on Stable }); await tx.wait(); console.log("Included at gas price:", tx.gasPrice?.toString()); ``` ```text Included at gas price: 1000000000 ``` **双重角色的 USDT0,移植风险。** 从以太坊移植的合约不应镜像原生余额,应拒绝向 `address(0)` 的转账,并且不应依赖 `EXTCODEHASH` 进行地址重用检测。 :::warning 在 Stable 上,移植一个在内部变量中镜像原生余额的合约是不安全的。一次外部的 `USDT0.transferFrom` 调用可以在不调用任何合约代码的情况下抽干合约的原生余额。请始终在转账时刻用 `address(this).balance` 进行偿付能力检查。 ::: 阅读更多:[与以太坊的差异](/cn/explanation/ethereum-comparison) · [Stable 上的合约](/cn/explanation/contracts-overview) · [USDT0 迁移清单](/cn/explanation/usdt0-behavior)。 ### 机密转账(计划中) Stable 有一个计划中的功能,用于零知识转账,可隐藏金额,同时对授权方保持可审计。该功能尚未上线。 阅读更多:[机密转账](/cn/explanation/confidential-transfer)。 ### 推荐的下一步 * [**快速开始**](/cn/tutorial/quick-start) — 连接到测试网并发送第一笔交易。 * [**USDT0 行为**](/cn/explanation/usdt0-behavior) — 将合约移植到 Stable 而不踩双重角色的坑。 * [**Gas 定价**](/cn/reference/gas-pricing-api) — 在 Stable 的费用模型上正确构造交易。 * [**生产就绪**](/cn/how-to/production-readiness) — 在发布到主网之前验证集成。 ## 概览 ### 全流程核心优化 区块链交易生命周期 一笔区块链交易从提交到最终确认,需要经历多个紧密相连的阶段。交易首先通过 **RPC** 提交,进入 **内存池(mempool)**,被打包进区块后通过 **共识机制** 验证,再由 **状态机(state machine)** 执行,最终写入 **数据库** 进行持久化。只有在完成整个流程后,用户才能收到确认结果。 仅优化某一个阶段是远远不够的。任何环节的低效都会影响系统整体性能。因此,Stable 致力于从上到下全面优化。 请查看以下页面,了解 Stable 如何从共识层、执行层、数据库层到 RPC 层,对各个架构模块进行系统性升级优化,以确保交易能稳定地以高性能处理。 ## 分配模块 ### 概述 `distribution` 预编译合约作为桥梁,使 Stable SDK 的 `x/distribution` 模块功能能在 EVM 环境中使用。 ### 目录 1. **[概念](#concepts)** 2. **[配置](#configuration)** 3. **[方法](#methods)** 4. **[事件](#events)** ### 概念 在 `distribution` 预编译合约中,会进行额外检查以确保委托者或存款者是调用者。 ### 配置 合约地址和gas费用已预定义。 #### 合约地址 * `0x0000000000000000000000000000000000000801` ### 方法 #### `setWithdrawAddress` 设置用于接收委托者向验证者委托代币奖励的地址。 有时,当委托者是自我委托时,验证者地址被用作委托者。 当提取者地址成功设置时,会发出 `SetWithdrawAddress` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ----------------- | ------- | --------- | | delegatorAddress | address | 委托者的地址 | | withdrawerAddress | address | 接收委托奖励的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | ------------------ | | success | bool | 如果提取者地址成功设置,则为true | #### `withdrawDelegatorRewards` 提取委托者从验证者那里应得的奖励。 验证者奖励给委托者的所有类型的代币都在单次交易中提取。 当奖励成功提取时,会发出 `WithdrawDelegatorRewards` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | delegatorAddress | address | 委托者的地址 | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------ | ------- | ------------- | | amount | Coin\[] | 委托者将获得的各种代币奖励 | `Coin` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ------ | ------- | --------------- | | denom | string | 奖励的denomination | | amount | uint256 | 奖励的数量 | #### `withdrawValidatorCommission` 提取验证者的佣金。 验证者作为佣金获得的所有类型的代币都在单次交易中提取。 当佣金成功提取时,会发出 `WithdrawValidatorCommission` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------ | ------- | ------------- | | amount | Coin\[] | 验证者将获得的各种代币佣金 | #### `validatorDistributionInfo` 返回代表验证者将获得奖励的分配信息。验证者可以在自己的地址上向自己委托代币,这被称为自绑定。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ---------------- | ------------------------- | -------- | | distributionInfo | ValidatorDistributionInfo | 验证者的分配信息 | `ValidatorDistributionInfo` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | --------------- | ---------- | --------- | | operatorAddress | address | 验证者操作员的地址 | | selfBondRewards | DecCoin\[] | 验证者的自绑定金额 | | commission | DecCoin\[] | 验证者的佣金 | `DecCoin` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | --------- | ------- | --------------- | | denom | string | 奖励的denomination | | amount | uint256 | 奖励的数量 | | precision | uint8 | 奖励的精度 | #### `validatorOutstandingRewards` 返回验证者的未结算奖励。未结算奖励表示由验证者的佣金和自绑定奖励以及委托者的总奖励组成的奖励总额。如果有验证者A,委托者B、C和D委托给A,那么验证者的未结算奖励是A的佣金和自绑定奖励 + B、C和D的奖励的总和。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---------- | --------- | | rewards | DecCoin\[] | 验证者的未结算奖励 | #### `validatorCommission` 返回验证者的佣金。此方法用于在调用 `withdrawValidatorCommission` 方法之前检索验证者的佣金。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ---------- | ---------- | ------ | | commission | DecCoin\[] | 验证者的佣金 | #### `validatorSlashes` 返回验证者在起始高度和结束高度之间的削减历史。削减是当验证者恶意行为或违反网络规则(如双重签名、不当行为或不遵循链规则)时施加的罚款。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证者的地址 | | startingHeight | uint64 | 起始高度 | | endingHeight | uint64 | 结束高度 | | pageRequest | PageReq | 分页请求 | `PageReq` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ---------- | ------ | ------- | | key | bytes | 分页的键 | | offset | uint64 | 分页的偏移量 | | limit | uint64 | 分页的限制 | | countTotal | bool | 是否计算总页数 | | reverse | bool | 是否反转分页 | ##### 输出参数 | 名称 | 类型 | 描述 | | ---------- | ---------------------- | ------ | | slashes | ValidatorSlashEvent\[] | 验证者的削减 | | pagination | PageResp | 分页响应 | `ValidatorSlashEvent` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | --------------- | ------ | ------ | | validatorPeriod | uint64 | 验证者的期间 | | fraction | Dec | 削减的分数 | `Dec` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | --------- | ------ | ------ | | value | uint64 | Dec的值 | | precision | uint8 | Dec的精度 | `PageResp` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ------- | ------ | ------- | | nextKey | bytes | 分页的下一个键 | | total | uint64 | 总页数 | #### `delegationRewards` 返回委托者从验证者那里获得的奖励。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托者的十六进制地址 | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---------- | -------------- | | rewards | DecCoin\[] | 委托者从验证者那里获得的奖励 | #### `delegationTotalRewards` 返回委托者从所有验证者那里获得的总奖励。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托者的十六进制地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---------------------------- | ----------------- | | rewards | DelegationDelegatorReward\[] | 委托者从所有验证者那里获得的总奖励 | | total | DecCoin\[] | 奖励的总额 | `DelegationDelegatorReward` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ---------------- | ---------- | -------------- | | validatorAddress | address | 验证者的地址 | | reward | DecCoin\[] | 委托者从验证者那里获得的奖励 | #### `delegatorValidators` 返回委托者绑定的验证者。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托者的十六进制地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ---------- | --------- | --------- | | validators | string\[] | 委托者绑定的验证者 | #### `delegatorWithdrawAddress` 返回通过 `setWithdrawAddress` 方法设置的接收委托奖励的地址。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ---------- | | delegatorAddress | address | 委托者的十六进制地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | --------------- | ------- | --------- | | withdrawAddress | address | 接收委托奖励的地址 | ### 事件 #### SetWithdrawAddress | 名称 | 类型 | 索引 | 描述 | | --------------- | ------- | -- | ----------- | | caller | address | Y | 调用者(委托者)的地址 | | withdrawAddress | address | N | 接收委托奖励的地址 | #### WithdrawDelegatorRewards | 名称 | 类型 | 索引 | 描述 | | ---------------- | ------- | -- | ------ | | delegatorAddress | address | Y | 委托者的地址 | | validatorAddress | address | Y | 验证者的地址 | | amount | uint256 | N | 奖励的数量 | #### WithdrawValidatorCommission | 名称 | 类型 | 索引 | 描述 | | ---------------- | ------- | -- | ------ | | validatorAddress | address | Y | 验证者的地址 | | commission | uint256 | N | 佣金的总数量 | ## EIP-7702 Stable 支持 EIP-7702,引入了统一账户模型,使外部账户(EOA)可临时以智能账户方式运行。 关键特性: * 用户可继续使用原有的私钥签名交易 * 执行时可临时具备智能合约逻辑,无需永久升级账户 * 实现安全的支付与托管授权流程 * 改善钱包与商户使用 USDT 时的体验 开发者: * 用户无需部署新合约即可获得合约级功能,降低接入门槛,简化托管逻辑。 ## 使用签名授权进行结算 ERC-3009 允许代币持有者通过签名消息来授权转账。任何人随后都可以提交该签名授权,在链上执行转账。发送方无需直接调用合约。 这是 Stable 上 [x402](/cn/explanation/x402) 支付背后的结算机制。 ### 它解决了什么问题? #### 授权额度问题 传统的 ERC-20 第三方转账模式是 `approve` + `transferFrom`。发送方首先调用 `approve` 授予支出额度,然后第三方调用 `transferFrom` 转移资金。这存在众所周知的问题: * **需要两笔交易**:发送方必须先发送一笔链上 `approve` 交易,任何转账才能进行。这需要消耗 gas 并增加延迟。 * **无限额度风险**:为了避免重复的授权交易,许多应用程序请求无限支出权限,从而产生重大安全风险。 ERC-3009 采用了不同的方法。发送方不授予额度,而是为特定转账签署一次性授权。无需单独的授权步骤,也不会留下持续的支出权限。 #### 顺序 nonce 问题 ERC-2612(`permit`)也支持签名授权,但它使用顺序 nonce。多个 permit 之间存在顺序依赖:如果 nonce 5 未被消耗,nonce 6 就永远无法执行。 ERC-3009 通过**唯一 nonce** 解决了这个问题。每个授权使用一个 32 字节的值,而不是顺序计数器。多个授权可以独立创建和提交,以任意顺序进行,彼此不相互依赖。 #### 对比 | **属性** | **ERC-20**(`approve`) | **ERC-2612**(`permit`) | **ERC-3009** | | :------- | :---------------------------- | :--------------------- | :----------------------------- | | 链上步骤 | 2(`approve` + `transferFrom`) | 1(`transferFrom`) | 1(`transferWithAuthorization`) | | 使用额度模型 | 必需(链上交易) | 是(通过 `permit` 设置额度) | 不需要(签名) | | Nonce 模型 | 顺序 | 顺序 | 唯一 | | 并发授权 | 否 | 否 | 是 | ### 工作原理 #### transferWithAuthorization 发送方签署一条包含转账详情的 EIP-712 类型化数据消息。任何人随后都可以使用该签名消息在代币合约上调用 `transferWithAuthorization`。合约验证签名、检查有效期窗口、执行转账,并将 nonce 标记为已使用。 签名授权包含: * `from`:发送方(签名者)的地址 * `to`:接收方的地址 * `value`:转账金额 * `validAfter`:此授权可执行的最早时间(Unix 时间戳) * `validBefore`:此授权可执行的最晚时间(Unix 时间戳) * `nonce`:确保唯一性的 32 字节值 时间窗口(`validAfter`/`validBefore`)让发送方可以精确控制转账可以发生的时间。授权可以安排在未来执行、设定截止时间,或两者兼具。如果窗口在提交之前过期,授权将变为无效,资金仍归发送方所有。 #### receiveWithAuthorization 此函数的工作方式与 `transferWithAuthorization` 完全相同,但多了一项检查:**调用者必须是接收方**。这可以防止抢跑攻击,即第三方观察到一笔待处理的授权并抢先提交以操纵交易顺序。 这在支付场景中很有用,因为应由接收方(商家或服务提供商)来发起结算。 #### cancelAuthorization 发送方可以在授权被执行之前撤销一个未使用的授权。发送方签署一条 EIP-712 取消消息,合约会将 nonce 标记为已使用而不执行转账。原始授权将无法再被提交。 ### 内置的安全特性 * **一次性使用**:每个唯一 nonce 只能使用一次。重复提交相同的签名授权将会回滚。 * **有时间限制**:`validAfter`/`validBefore` 窗口确保授权不会无限期有效。 * **自包含**:一个签名授权一笔特定转账,转给一个特定接收方、转一个特定金额。不会留下持续权限。 * **非托管**:提交者从不持有发送方的资金。转账在合约内部直接从发送方转到接收方。 ### Stable 上的 ERC-3009 Stable 上的 USDT0 原生实现了 ERC-3009。任何应用程序都可以使用 `transferWithAuthorization`,无需部署额外的合约或中继基础设施。 #### 单资产结算 在以太坊上,即便使用 ERC-3009,提交者仍需要 ETH 来支付调用 `transferWithAuthorization` 的 gas。转账本身是 USDT,但执行却依赖于另一种原生资产。 在 Stable 上,USDT0 同时充当支付代币和 gas 代币。从授权到链上结算,整个支付生命周期都运行在单一稳定币上。在任何步骤中都不需要单独的原生资产。 正是这一特性使 Stable 上的 ERC-3009 成为更高层支付协议的坚实基础。[x402](/cn/explanation/x402) 直接利用了这一点,在标准 HTTP 通信中将 ERC-3009 用作其链上结算机制。 ### 要点总结 * ERC-3009 允许代币持有者通过签名消息来授权转账。任何人都可以提交该签名授权来执行转账。 * 它用一次性、自包含的授权取代了 ERC-20 额度模型。无需 `approve` 步骤、无持续权限、无双花风险。 * 唯一 nonce 允许以任意顺序并发创建和提交多个授权。 * Stable 上的 USDT0 原生支持 ERC-3009,由于仅使用 USDT0 即可完成结算,它为 x402 提供了实用的基础。 **另请参阅:** * [USDT 作为 Gas](/cn/explanation/usdt-as-gas-token) * [USDT0 在 Stable 上的行为](/cn/explanation/usdt0-behavior) * [x402(HTTP 原生支付)](/cn/explanation/x402) ## 以太坊对比 Stable 完全兼容 EVM,因此大多数以太坊工具、库和合约模式无需修改即可使用。下面的章节将逐一介绍从以太坊迁移到 Stable 时哪些保持不变、哪些会改变。 ### 哪些保持不变 Stable 与以太坊开发生态系统保持完全兼容: | **领域** | **兼容性** | | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | 语言 | Solidity、Vyper | | 工具 | Hardhat、Foundry | | 库 | ethers.js、web3.js | | 合约模式 | 所有标准 EVM 约定(ERC-20、ERC-721、ERC-1155、代理等) | | RPC 接口 | 支持大多数 `eth_*` 方法(`eth_call`、`eth_sendRawTransaction`、`eth_getBalance`、`eth_getLogs`、`eth_estimateGas` 等)。完整列表请参见 [JSON-RPC API](/cn/reference/json-rpc-api) | 现有的智能合约、部署脚本和前端集成只需更改 RPC 端点和链 ID 即可面向 Stable。 ### 哪些不同 有四种行为与以太坊不同。 #### 1. 单槽最终性 以太坊需要多个区块确认才能将交易视为最终确认。Stable 提供单槽最终性:交易一旦被包含在某个区块中即为最终确认。 对开发者而言,这意味着: * 交易一旦出现在已确认的区块中,其状态更改即为最终且不可逆转。 * 应用程序可以安全地依赖区块包含作为结算确认。 即便具备确定性最终性,处理金融敏感流程的应用程序仍应: * 在进行依赖性操作(例如解锁、赎回)之前,通过 RPC 或发出的事件验证交易是否成功。 * 为自动化和批量操作实现重试和对账逻辑,以处理临时提交或 RPC 错误。 #### 2. Gas 代币:USDT0 在 Stable 上,交易费用以 USDT0 支付,而非波动性的原生代币。这提供了以 USDT 计价、可预测的低 gas 成本。 * 用户需要在其钱包中持有 USDT0 才能提交交易。 * 交易中的 `value` 字段仍可用于发送 USDT0,类似于在以太坊上发送 ETH 的方式。 * 详情请参见 [USDT 作为 gas](/cn/explanation/usdt-as-gas-token)。 #### 3. 无优先小费 Stable 使用单组件 gas 模型。不存在基于小费的交易排序。 * `maxPriorityFeePerGas` 被忽略(始终为 0)。 * 交易排序不受费用竞价影响。 * 钱包应隐藏或禁用优先小费输入字段。 * 详情请参见 [Gas 定价](/cn/explanation/gas-pricing)。 #### 4. USDT0 双重角色行为 USDT0 既作为原生 gas 代币,又作为 ERC-20 代币。这在余额语义、授权安全性以及某些操作码假设方面引入了行为差异。完整详情请参见 [USDT0 在 Stable 上的行为](/cn/explanation/usdt0-behavior)。 ### 快速对比 | **参数** | **Stable** | **以太坊** | | :--------------------------- | :--------- | :------ | | Gas 代币 | USDT0 | ETH | | 最终性 | 单槽 | 多区块确认 | | 出块时间 | \~0.7 秒 | \~12 秒 | | 优先小费(`maxPriorityFeePerGas`) | 被忽略(始终为 0) | 用于排序 | | EIP-1559 交易格式 | 支持 | 支持 | | EVM 兼容性 | 完全 | 不适用 | ### 推荐的后续内容 * [**USDT 作为 gas**](/cn/explanation/usdt-as-gas-token) — 了解取代 ETH 作为 gas 的资产模型。 * [**Gas 定价**](/cn/explanation/gas-pricing) — 详细了解单组件费用模型。 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 针对双重角色资产语义、授权安全性和 `EXTCODEHASH` 行为审计合约。 ## 与以太坊生态的兼容性 Stable 完全兼容 EVM,使开发者可直接使用以太坊工具、库和智能合约模块,无需进行任何修改。 **兼容特性:** * 语言: 支持 Solidity 与 Vyper。 * 工具链: 原生兼容 Hardhat、Foundry。 * 库: 兼容 ethers.js、web3.js 等常见客户端。 * 合约模块: 遵循 ERC-20、事件机制、访问控制等标准规范。 * RPC 接口: 提供与以太坊相同的 JSON-RPC 方法,支持无缝迁移。 ## 执行层 ### Stable EVM Stable EVM **Stable EVM** 是 Stable 区块链的以太坊虚拟机兼容执行层,使用户能够通过现有的以太坊工具和钱包(如 MetaMask)无缝与链进行交互。Stable EVM 结合了 EVM 的开发体验与 StableSDK 的模块化、高性能基础设施。 Stable 的以太坊兼容执行层,支持使用现有的以太坊工具与钱包无缝交互。同时 Stable EVM 引入一系列 预编译合约,允许 EVM 智能合约安全且原子性地调用Stable底层接口。通过这种设计,智能合约可执行包括代币转账、质押、参与治理等特权操作。 ### 未来路线图一:Optimistic并行执行(OPE) 传统区块链系统依赖顺序执行机制,逐个处理交易以确保所有节点的一致状态。虽然这种方式保证了确定性,但极大限制了吞吐量和可扩展性,尤其在现代区块链需要支持每秒成千上万笔交易时更为突出。 为突破此瓶颈,Stable 将采用 **Block-STM**,一种已验证的并行执行引擎,支持 **Optimistic并行执行(Optimistic Parallel Execution,简称 OPE)**,允许在保证确定性的前提下并行处理交易,显著提升性能。 #### Block-STM 的工作原理 Block-STM 使用Optimistic并发控制机制:首先假设交易之间不会冲突并进行并行执行,随后进入验证阶段检查冲突并处理重执行。核心依赖以下五项关键技术: **1. 多版本内存结构** Block-STM 为每个内存键(memory key)存储多个版本: * 每个交易读取先前交易已提交的最新版本; * 执行过程中,读取和写入都会进行版本标记; * 验证阶段会检查这些版本以确认是否存在冲突。 **2. 基于读取集(Read-Set)/写入集(Write-Set)的验证机制** * 执行时,交易会记录其读取的键及其版本(Read-Set); * 执行结束后,其写入操作记录为 Write-Set; * 验证阶段,若任意 Read-Set 中的键在执行期间被其他交易修改,则该交易判定为冲突,程序中止然后进行重试并增加尝试编号(incarnation)。 **3. 通过 ESTIMATE 标记实现快速冲突检测** * 失败的交易会将其 Write-Set 标记为 ESTIMATE; * 若其他交易读取到 ESTIMATE 标记的值,会立即停止并等待该交易重试(触发 `READ_ERROR`); * 该机制可快速识别依赖关系,减少不必要的执行负担。 **4. 预设交易顺序** * 区块内的所有交易按照预设的确定顺序执行; * 验证与提交阶段也遵循上述相同顺序; * 即便并行执行,这能确保所有节点最终状态仍一致。 **5. 任务调度器(Collaborative Scheduler)** * 任务调度器以线程安全的方式为执行和验证分配任务; * 优先处理索引较早的交易,加速处理早期提交的任务、减少重试; * 调度器管理已多次尝试的任务,直到成功提交。 #### Block-STM 的核心优势 * **无锁并行**:基于 MVCC(多版本并发控制),允许多个交易同时读取/写入,无需加锁。在执行后才进行冲突检查,最大限度提升吞吐量。 * **ESTIMATE 标记机制**:失败交易通过 ESTIMATE 标记提前通知依赖交易暂停,避免资源浪费。 * **高效调度与优先提交机制**:通过任务调度器优先提交索引较早的交易,提高整体吞吐并缩短执行周期。 * **确定性与共识兼容性**:即使某些交易需要重试,也将在相同顺序下提交,确保所有节点达成相同的最终一致性。 #### Stable 上的 OPE Optimistic Parallel Execution on Stable Stablechain 将把 **Optimistic并行执行(OPE)** 作为其执行层的核心功能,并结合 **Optimistic区块处理(Optimistic Block Processing,简称 OBP)**。需要注意的是,OPE 与 OBP 是互补但本质不同的两种优化策略。 #### 关于 OBP * OBP 并不涉及并行,而是优化执行时机; * 在 `ProcessProposal` 阶段,Stable 会在交易 gossip 的同时预先执行区块; * 执行结果缓存在内存中,在 `FinalizeBlock` 阶段复用,节省时间并避免重复计算。 通过结合 OPE 与 OBP,Stable 能在高交易负载下最大限度降低执行延迟与资源争用,带来卓越性能表现。 #### 性能预期 内部基准测试表明,结合 **基于 Block-STM 的 OPE** 与 **StableDB**,Stable 的交易处理吞吐量有望提升 **至少 2 倍**。 ### 未来路线图二:StableVM++ 虽然 OPE 与 OBP 聚焦于优化「并行执行多笔交易」,但另一个关键的提升性能手段是:「如何更高效地处理每一笔交易」。 Stable 正在探索替代的 EVM 实现方式以加速交易执行。在当前选项中,**EVMONE**(一个高性能 C++ 编写的 EVM)是最具潜力的替代现有 Go 实现的EVM。理论基准测试表明,这一更换预计将带来高达 **6 倍的 EVM 执行性能提升**。 ## 终局性规则与兼容性保证 Stable 的交易在 EVM 执行环境中处理。当交易被打包入区块后,其状态变更立即生效。 #### 确认规则: * 交易被区块打包即视为确认 * 状态的变更(余额、存储、事件)可通过 RPC 查询 #### 结算特性: * Stable 实现单槽终局性(single-slot finality): * 交易一旦被有效区块打包即不可逆。 * 开发者可直接将区块打包视为结算完成。 #### 兼容性: * 系统模块接口与执行行为在测试网期间保持稳定 * 任何潜在的破坏性变更将提前公告并记录于变更日志 * 发布时附带迁移指引 * 后续版本将引入正式的兼容性政策与变更分类机制 ## 资金流转 Stable 是首条专为稳定币支付打造的区块链。该网络针对高吞吐、低延迟的稳定币交易进行了优化,提供 P2P 支付和商户收款,并以 USDT 实现即时结算。应用层的 gas 赞助与减免使服务提供商能够为终端用户提供零手续费的体验,带来主流支付网络般的感受,同时屏蔽了区块链系统的复杂性。 本页介绍 Stable 上资金的完整生命周期:USDT 如何进入网络、在参与者之间流转,以及如何退出回归法币通道。 ### 1. 客户存款(入金) 用户通过以下三种主要渠道之一将资金引入网络: * **加密货币转账**:任何主流加密货币在 Stable 上被桥接或转换为 USDT0。USDT0 是 USDT 的全链标准,也是网络上的主要形态。 * **法币入金**:通过银行卡、ACH 或本地支付方式将法币转换为 USDT0,直接发送到用户的钱包。 * **CEX 提现**:用户从支持的中心化交易所提现 USDT,选择 Stable 作为目标网络。交易所直接结算到用户的钱包。 在所有情况下,最终状态都相同:用户的钱包直接在 Stable 上持有 USDT(以 USDT0 形式)。 ### 2. P2P / 商户转账(链上收款) 资金进入 Stable 后,客户即可将 USDT 直接发送给另一位用户或商户。链上转账的关键特性: * **即时结算**:转账在链上立即完成。 * **非托管**:在使用非托管钱包的情况下,源地址与目标地址之间的用户余额绝不会经过任何 PSP 或中介之手。 * **单一资产**:由于 USDT 既是 gas 资产又是结算资产,流程中没有额外的代币,也没有隐藏的价差。 * **零 gas 选项**:gas 减免使终端用户无需管理区块链费用即可转移资金。详见 [Gas 减免](/cn/reference/gas-waiver-api)。 ### 3. 用户 / 商户余额 商户在自己直接掌控的 Stable 钱包中接收 USDT。资金在用户或商户的托管下保存于链上。这些钱包可由支付服务提供商代表用户创建和管理。 ### 4. 商户提现(出金 / 付款) 当商户或用户请求链下法币结算时: 1. 服务提供商通过银行或付款通道发起转换(USDT → 法币)。 2. 资金被记入商户所选的账户。 服务提供商仅在为商户兑现时才重新进入流程,而不参与生态系统内部的转账。日常的 P2P 流转无需任何中介;服务提供商仅在存款(向商户账户转入 USDT)或提现(USDT → 法币)时参与。 ### 跨资产交易 Stable 还支持付款方持有非 USDT 加密货币的场景。 #### 用户兑换为另一种加密货币 用户可以通过集成的交易所、经纪商或链上 DEX 持有或兑换为另一种加密货币(例如 BTC 或 ETH)。在付款时,系统会自动将所选的加密货币转换为 USDT,然后将其传输到商户的 Stable 钱包。无论用户偏好哪种资产,所有链上结算都始终以 USDT 进行。 #### 商户接受加密货币付款 商户无需直接接受或管理多种加密货币。他们始终在自己的 Stable 钱包中收到 USDT,从而在整个网络中保持单一结算货币。这种设计将商户的外汇风险降至最低,并简化了对账和报告工作。 #### 服务提供商在转换中的角色 转换逻辑(例如 BTC → USDT)可由交易所合作伙伴、流动性提供商或支付服务提供商自有的资金部门处理。商户始终免受波动性或流动性风险的影响;他们只会收到 USDT。 ### 下一步推荐 * [**USDT 作为 gas**](/cn/explanation/usdt-as-gas-token) — 了解 USDT0 如何在 Stable 上同时作为原生 gas 和 ERC-20 余额。 * [**桥接到 Stable**](/cn/explanation/usdt0-bridging) — 了解 USDT0 如何通过 OFT Mesh 或 Legacy Mesh 从其他链转移到 Stable。 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 使用标准 EVM 工具在测试网上提交一笔 USDT0 转账。 ## Gas 定价机制 Stable 采用单一组成部分的 Gas 费模型: * **不支持优先费用( 禁用maxPriorityFeePerGas)** * 费用仅基于基础的执行成本 * 以 **USDT0** 支付 设计理由: * 消除 Gas 波动,保证支付可预测性 * 简化用户体验,避免通过支付额外报酬使验证节点优先处理交易 开发者注意: * 不支持指定优先费用以加速交易 * 钱包应隐藏或禁用优先费用的设置 * Gas 预估需基于 Stable RPC 的定价预测 ## Gas 豁免人 ### 摘要 Gas 豁免人(免 Gas 费)功能通过允许一小部分经过治理批准的地址("豁免人")提交 `gasPrice = 0` 的交易,实现 Stable 链上终端用户的无 Gas 费交易。Stable 目前运营一个豁免人服务("豁免人服务器"),合作伙伴可以集成该服务,无需实现特定协议的封包逻辑即可提供无 Gas 费用户体验。 本文档规定了 Gas 豁免人 机制、交易格式、治理控制以及面向合作伙伴的豁免人服务器 API。 ### 范围 本规范涵盖: * 免 Gas 费交易的协议级规则 * 封包交易机制和标记地址 * 治理控制的授权和允许的目标 * 用于提交已签名用户交易的豁免人服务器接口 ### 定义 * **Waiver(豁免人)**:通过验证者治理在链上注册的以太坊地址,授权提交免 Gas 费交易。 * **InnerTx(内部交易)**:终端用户签名的交易,`gasPrice = 0`。 * **WrapperTx(封包交易)**:由豁免人签名的交易,将用户的 `InnerTx` 传输到链上并授权执行。 * **Marker address(标记地址)**:用于识别豁免人封包交易的哨兵地址:`0x000000000000000000000000000000000000f333`。 * **AllowedTarget(允许的目标)**:一种策略,将豁免人限制在特定的合约地址和方法选择器。 ### 概述 Gas 豁免人 使用封包交易模式: 1. 用户使用 `gasPrice = 0` 签署 `InnerTx`。 2. 豁免人将 `InnerTx` 封包成 `WrapperTx` 并广播。 3. 验证者检测到标记交易,验证豁免人授权和策略约束,然后执行嵌入的 `InnerTx`。 Stable 运营一个豁免人服务(豁免人服务器),该服务在链上注册为授权豁免人。合作伙伴通过豁免人服务器 API 集成,提交已签名的 `InnerTx` 负载。 ### 协议规范 #### 标记地址路由 当且仅当满足以下条件时,交易被视为豁免人封包交易: * `to == 0x000000000000000000000000000000000000f333`。 协议将交易的 `data` 字段解释为编码的内部交易负载,并使用以下豁免人验证规则进行处理。 #### 授权和策略检查 对于每个候选封包交易,验证者必须执行: 1. **豁免人授权** * `WrapperTx.from` 必须是通过治理在链上注册的豁免人地址。 2. **Gas 豁免** * `WrapperTx.gasPrice` 必须等于 `0`。 * `InnerTx.gasPrice` 必须等于 `0`。 3. **目标白名单** * `InnerTx.to` 和从 `InnerTx.data` 提取的方法选择器必须被豁免人的 `AllowedTarget` 策略允许。 4. **Value 限制** * `WrapperTx.value` 必须等于 `0`。 若以上任何一条未通过,则封包交易将被拒绝,封包内部的交易也无法执行。 #### 执行语义 如果所有检查通过: 1. 协议以用户身份执行 `InnerTx`,保留用户的 `from`、`nonce` 和调用语义。 2. Gas 计费由豁免人机制处理:用户不支付 Gas 费,豁免人交易根据功能定义使用 `gasPrice = 0`。 3. 封包交易必须提供足够的 `gasLimit` 以覆盖 `InnerTx` 的执行(包括解包和验证的开销)。 ### 交易格式 #### WrapperTx 封包交易由豁免人签署并发送到标记地址。 ```javascript WrapperTx { from: waiver_address, to: 0x000000000000000000000000000000000000f333, value: 0, // 必须为零 data: RLP(InnerTx), // RLP 编码的内部交易 gasPrice: 0, // 必须为零 gasLimit: sufficient_for_inner, // 必须覆盖内部执行 + 开销 nonce: waiver_nonce } ``` #### InnerTx 内部交易由终端用户签署。 ```javascript InnerTx { from: user_address, to: target_contract, value: value, data: call_data, gasPrice: 0, // 必须为零 gasLimit: execution_gas, nonce: user_nonce } ``` ### 治理控制的访问 豁免人授权由验证者治理在链上管理。 治理控制提供: * 可审查的豁免人地址授权 * 豁免人注册和更新的链上透明度 * 撤销能力 * 通过 `AllowedTarget` 进行每个豁免人的范围界定 ### 安全模型 #### 终端用户签名完整性 用户签署 `InnerTx`。豁免人不允许修改内部交易负载而不使签名失效。合作伙伴仍必须确保用户仅签署预期的交易负载。 #### 信任边界 如果合作伙伴通过豁免人服务器路由提交,Gas 豁免人 引入了服务依赖性: * 服务的可用性影响提交无 Gas 费交易的能力。 * 授权保留在链上;只有注册的豁免人地址才能产生有效的封包提交。 ### 合作伙伴集成 合作伙伴通过以下方式集成: 1. 从用户收集已签名的 `InnerTx`(`gasPrice = 0`)。 2. 将已签名的内部交易提交到豁免人服务器 API。 3. 处理流式结果并向终端用户显示交易哈希。 ### 豁免人服务器 #### 概述 豁免人服务器将已签名的用户 `InnerTx` 负载封包并广播为豁免人授权的封包交易。合作伙伴无需构造封包交易或操作豁免人地址。 #### 端点和基础 URL 基础 URL: * 主网:待定 * 测试网:`https://waiver.testnet.stable.xyz` #### 身份验证 除健康检查外,所有端点都需要 Bearer Token 身份验证: ``` Authorization: Bearer ``` #### API ##### GET `/v1/health` 健康检查端点。 身份验证:无。 ##### POST `/v1/submit` 提交一批已签名的内部交易。 身份验证:必需(`Bearer`)。 请求正文: ```json { "transactions": ["0x", "0x"] } ``` 响应以 NDJSON(换行符分隔的 JSON)流式传输。每行对应一个提交的交易索引。 示例: ```json {"index":0,"id":"abc123","success":true,"txHash":"0x..."} {"index":1,"id":"def456","success":false,"error":{"code":"VALIDATION_FAILED","message":"invalid signature"}} ``` ##### GET `/v1/submit` 用于流式提交的 WebSocket 接口。 身份验证:必需(`Bearer`)。 #### 集成示例 ```javascript const WAIVER_SERVER = "https://waiver.testnet.stable.xyz"; async function submitGaslessTransaction(signedInnerTxHex, apiKey) { const response = await fetch(`${WAIVER_SERVER}/v1/submit`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, body: JSON.stringify({ transactions: [signedInnerTxHex], }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).trim().split("\n"); for (const line of lines) { const result = JSON.parse(line); console.log(result); } } } ``` #### 创建用户 InnerTx 合作伙伴负责构造 `gasPrice = 0` 的 `InnerTx`,然后收集用户签名。 示例: ```javascript import { ethers } from "ethers"; async function createInnerTx(userWallet, contractAddress, callData, nonce) { const innerTx = { to: contractAddress, data: callData, value: value, gasPrice: 0, // 豁免人必须为 0 gasLimit: 100000, nonce: nonce, chainId: 2201, // 主网为 988,测试网为 2201 }; return await userWallet.signTransaction(innerTx); } ``` #### 错误代码 * `PARSE_ERROR`:解析交易失败 * `INVALID_REQUEST`:请求正文格式错误 * `BATCH_SIZE_EXCEEDED`:批处理大小超过允许的最大值 * `VALIDATION_FAILED`:交易验证失败 * `BROADCAST_FAILED`:广播到链失败 * `RATE_LIMITED`:超过速率限制 * `QUEUE_FULL`:服务器队列已满 * `TIMEOUT`:请求超时 ## 保证区块空间(Guaranteed Blockspace) ### 企业需要稳定的支付基础设施 随着稳定币的持续发展,越来越多的企业将其纳入财务操作中,例如支付、资金调拨和跨境结算。这一趋势在那些获取稳定法币受限的地区尤为明显。在非洲、拉丁美洲等通货膨胀严重、货币管控严格的市场,稳定币正逐渐成为这些地区企业持续运营的关键工具。 当前,大部分稳定币交易发生在通用型公链上,例如 Ethereum、Solana 和 Tron。这些网络虽提供了高可组合性和智能合约支持,但它们并未针对**费用可预测性**或**执行确定性**进行专门设计。 * **Ethereum**:2022 年 5 月 1 日,Yuga Labs 的 “Otherside” NFT 铸造事件在 Ethereum 上燃烧了超过 2 亿美元的 Gas 费,峰值 Gas 价格超过 8,000 gwei,造成整个网络交易费用剧烈波动,缺乏可预测性。 * **其他区块链网络**:在 Solana 和 Base 等低费用网络中,由于存在 MEV 和套利机会,激励机制导致大量交易垃圾(spam)泛滥。自动化机器人频繁向链上提交交易以从中捕获价值,从而造成网络拥堵,影响真实用户的正常使用。 ![来源:Flashbots 与 Robert Miller 联合发布的《MEV 与扩容的极限》报告](/images/share-of-gas.png) *来源:Flashbots 与 Robert Miller 联合发布的《MEV 与扩容的极限》报告* 若企业要大规模采用稳定币进行支付,其底层基础设施必须具备**支付可靠性**。这意味着必须在任何网络条件下都能提供可预测的交易速度与稳定的交易费用。否则,基于通用区块链的稳定币支付将难以满足企业级应用的要求。 ### 保证区块空间(Guaranteed Blockspace) 为保障企业支付操作的稳定性与可靠性,Stable 将推出 **保证区块空间(Guaranteed Blockspace)** 支持。 保证区块空间(Guaranteed Blockspace) 是一种**专属的区块容量分配机制**,旨在为企业客户预留固定比例的区块空间,无论网络整体负载如何。关键性的交易(如工资发放、结算清算、供应商支付)都可在可预测的交易时间与成本下完成执行。 该机制通过以下方式实现: * **专属内存池(Guaranteed Mempool)**:验证节点会从独立的企业交易内存池中提取优先级高的交易,避免与一般交易争夺资源。 * **验证节点级别的处理流程**:每个验证节点为企业用户预留固定比例的区块空间,确保这类交易能以高确定性地被包含进块中。 * **专属 RPC 服务**:企业级 API 会通过独立的 RPC 服务器发送交易,减少资源争抢,提升吞吐稳定性。 保证区块空间为商业用户带来以下优势: * **专属交易通道**:通过独立的交易传输路径,确保交易享有优先访问专属区块空间的权限。 * **交易执行保障**:即使在高负载网络条件下,每个区块内都能确保企业交易的优先执行权。 * **保持网络去中心化特性**:不影响验证节点的开放性或网络参与者的操作自由。 * **关键操作的链上性能保障**:即使网络拥堵,企业的关键链上交易依旧可以保持可靠、高效运行。 ## 高性能 RPC 在构建高性能区块链的过程中,仅仅优化共识机制或出块速度是不够的。**RPC 层** 是区块链与用户之间的接口,是实现端到端用户体验的关键组成部分。Stable 提出一种高性能 RPC 架构,旨在突破传统 RPC 的性能瓶颈。 ### 为什么高性能 RPC 至关重要 #### 用户连接区块链的入口 **RPC** 是用户与区块链交互的主要方式: * 钱包通过 RPC 广播交易; * DApps 借助 RPC 查询链上状态来渲染界面、准备交易、进行模拟、获取事件和日志等; * 区块浏览器、索引服务和交易机器人程序都依赖 RPC 实时获取数据。 即使底层区块链处理交易的速度极快、出块迅速,如果 RPC 响应缓慢、延迟高,用户的整体体验仍将受到影响。事实上,在现实使用中,RPC 常常成为链上体验的性能瓶颈。 因此,Stable 的高性能区块链路线图中将 **优化RPC架构** 明确作为优先任务。 ### 传统 RPC 架构的弊端 #### 单一的架构设计与资源争用 Traditional RPC Architecture 传统上,RPC 节点通常是功能扩展的全节点,它们同时负责: * 在同一时间处理区块链的同步与RPC请求; * 若要扩展 RPC 能力,只能新增完整节点,导致必须重复执行状态同步、共识配置等繁重流程; * 共识、执行与 RPC 服务共用同一 CPU、内存与磁盘资源。一旦某一部分高负载,**将会拖慢其他模块的性能**,例如在交易高峰期,RPC 延迟大幅上升。 此外,传统架构通常并不区分读取与写入请求的处理方式。即使读取请求(如 `eth_getBalance`)在调用量上远超写入交易,两者仍在同一逻辑结构中混合处理,造成系统整体效率低下、可扩展性差。 ### Stable 的 RPC 架构 Stable 提出了 **路径分离(Split-Path)RPC 架构**,将读取与写入操作分离,并分别进行独立优化。 Stable RPC Architecture #### 核心原则 * 将 RPC 按功能拆分为多个轻量高效的节点; * 使用轻量级 RPC 边缘节点进行扩展,以提升系统可扩展性; * 根据不同功能优化路径,通过更高效的数据结构来减少请求延迟,实现更直接的链上数据访问或管理。 #### 性能提升 在读取路径下,Stable 的内部测试结果显示: * 单节点吞吐量超 **10,000 次/秒(RPS)**; * 同一环境下,端到端延迟 **低于 100ms**; * 边缘节点支持线性扩展,**无需状态同步或共识负担**。 Stable 的新型 RPC 架构在高流量场景下仍可维持流畅、迅捷的用户交互体验。 ### 未来工作方向 #### 优化 EVM View 查询 一个备受关注的研究方向是:**为 `eth_call` 等 EVM 只读操作提供专有支持**: * 此类调用不涉及状态更新或交易确认; * 可在轻量、无状态的运行环境中执行,只需当前状态快照; * 未来可设计专门针对 `eth_call` 的 RPC 节点,进一步降低响应时间,同时减轻主节点负担。 #### 全节点原生集成 Indexer 通过将索引器(Indexer)原生集成至全节点,可以显著加快向 DApps 提供数据的速度: * 当前架构:Node → RPC → Indexer(如 The Graph)→ 存储 → DApp; * 提议架构:集成 Indexer 的 Node → 数据库 → DApp; * 由于省略了额外的网络通信步骤,索引数据可被原生调用,大幅提升查询效率与实时性。 ## 概览 Stable 是一条兼容 EVM 的 Layer 1 区块链,其中 USDT0 是原生 gas 代币。大多数以太坊工具、库和合约模式无需修改即可使用。你只需将 RPC 指向 Stable 并切换 chain ID 即可连接。 ### 连接与充值 * [**连接**](/cn/reference/connect) — 主网和测试网 chain ID、RPC 端点、区块浏览器。 * [**为你的测试网钱包充值**](/cn/how-to/use-faucet) — 通过水龙头获取测试网 USDT0,或从 Sepolia 跨链桥接。 * [**Stable SDK**](/cn/explanation/sdk-overview) — 使用类型化的 TypeScript 客户端进行转账、桥接和兑换。 ### 使用 USDT0 构建 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 原生和 ERC-20 转账的 TypeScript 示例。 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 双角色余额对账、合约设计要求、迁移清单。 * [**与以太坊的差异**](/cn/explanation/ethereum-comparison) — 单槽最终性、USDT0 gas、无优先费小费。 * [**零 gas 交易**](/cn/how-to/integrate-gas-waiver) — 通过 Waiver Server API 集成 Gas Waiver。 ### 支付 * [**ERC-3009**](/cn/explanation/erc-3009) — 带授权的转账:链上结算原语。 * [**x402**](/cn/explanation/x402) — HTTP 原生支付,无需账户或 API 密钥。 * [**P2P 支付**](/cn/reference/p2p-payments) — 原生和 ERC-3009 委托转账。 ### 生态系统 各类提供商和基础设施已在 Stable 上运行:跨链桥、[官方 Uniswap v3 部署](/cn/reference/dexes)、预言机、RPC、钱包、托管等等。浏览[生态系统](/cn/reference/bridges)章节查看完整列表。 ## 核心功能 Stable 是一条高性能区块链,专为支持 USDT 相关活动而设计。基于 **委托权益证明(dPoS)** 机制构建,Stable 实现了 **完全兼容 EVM**,并达成 **亚秒级出块时间**,确保交易快速可靠地达成最终性。作为一个 **专注于 USDT 的网络**,Stable 提供了一系列优化用户体验的 USDT 专属功能。 主要功能包括: * **亚秒级交易最终性**:实现亚秒级出块和单轮次最终确认 * **100% EVM 兼容性**:支持所有 Ethereum 工具和智能合约 * **USDT 作为 Gas 代币**:USDT0 作为其原生 Gas 代币。USDT0 同时作为用于支付 Gas 和进行价值转移的原生资产,并作为支持 `approve`、`transfer`、`transferFrom` 和 `permit` 的 ERC20 代币。 * **USDT0 跨链转移**:支持从包括 Ethereum、Arbitrum、HyperEVM 等 EVM 区块链,以及 Tron 等其他区块链通过跨链桥转移 USDT0 * **Stable Pay 提供 Web2.5 用户体验**:通过 Stable Pay 实现无缝的链上交互体验 未来,Stable 还将推出更多功能,以进一步提升 USDT 的可用性和网络效率。其中包括 **USDT Transfer Aggregator(USDT 转账聚合器)**,可将多个 USDT 转账打包为单一交易以提升处理效率;以及 **企业专用区块空间 Enterprise Blockspace**,为机构用户提供可预测、稳定的 USDT 使用环境。 ## 学习 ### 基础 * [**概览**](/cn/explanation/overview) — Stable 是什么,以及如何阅读本文档。 * [**核心特性**](/cn/explanation/key-features) — 关键规格:单槽最终性、USDT0 作为 gas、完全 EVM 兼容。 * [**与以太坊的区别**](/cn/explanation/ethereum-comparison) — 从以太坊移植时,哪些保持不变,哪些会发生变化。 * [**核心概念**](/cn/explanation/core-concepts) — USDT0 的双重角色、有保障的区块空间、转账聚合器、最终性。 ### USDT0 行为 * [**USDT0 在 Stable 上的行为**](/cn/explanation/usdt0-behavior) — 双重角色余额、对账事件以及合约设计规则。 * [**USDT 作为 gas**](/cn/explanation/usdt-as-gas-token) — Stable 为何使用 USDT0 支付 gas,以及这对费用意味着什么。 * [**资金流转**](/cn/explanation/flow-of-funds) — USDT 在 Stable 上端到端的流动方式。 * [**USDT0 特性**](/cn/explanation/usdt-features-overview) — 每一项 USDT0 专属特性及其链接。 ### 架构 * [**技术概览**](/cn/explanation/tech-overview) — 在一个页面中介绍共识、执行、数据库和 RPC 各层。 * [**核心优化**](/cn/explanation/core-optimization-overview) — 实现亚秒级最终性背后的性能工作。 * [**最终性**](/cn/explanation/finality) — 单槽最终性、重组行为,以及"已确认"的含义。 * [**Gas 定价**](/cn/explanation/gas-pricing) — 以 USDT0 计价的仅基础费用模型。 ### 用例叙述 * [**支付**](/cn/explanation/use-case-payments) — Stable 为何适合 P2P、订阅、发票和按次付费。 * [**薪资发放**](/cn/explanation/use-case-payroll) — 在 Stable 上批量和定时执行薪资发放。 * [**代付交易**](/cn/explanation/use-case-sponsored) — 让应用为其用户承担 gas。 * [**私密转账**](/cn/explanation/use-case-private) — 即将推出的保密支付流程。 ## MPP 会话 会话是一种 MPP 支付意图,它将许多小额支付批量合并为单笔链上结算。客户端只需向托管合约存入一次资金,然后为每个请求签署廉价的链下凭证。只有净额会在链上结算,这使得流式工作负载中每个请求的亚分级经济模型成为可能。 ### 会话的工作原理 1. **存款。** 客户端将预算转入结算层上的会话托管合约。托管合约持有资金,并暴露一个结算函数,用于向卖方付款并退还剩余部分。 2. **每个请求一个凭证。** 对于每个付费请求,客户端签署一个携带 `(sessionId, cumulativeAmount, nonce, expiry)` 的链下凭证。服务器会检查累计金额是否单调递增且在存入余额范围内。此步骤无需任何链上操作。 3. **结算。** 在会话结束时或按配置的节奏,协调者将最新的凭证提交给托管合约。托管合约向卖方支付累计金额,并将剩余余额返还给客户端。只有这笔交易会触及链上。 当最新凭证被结算或凭证过期时,会话即告完成。 ### 何时使用会话还是单次扣费 | **工作负载** | **最佳意图** | | :----------------------------------------------- | :------- | | 按 token 计费的 LLM 推理、按帧计费的视频、实时数据流。向同一卖方进行的许多小额支付。 | 会话 | | 一次性付费 API 调用、单次购买资源、每笔交易独立的代理间商务。 | 单次扣费 | 盈亏平衡点取决于每个请求的链上结算相对于请求价格的昂贵程度。一旦你在 gas 上支付的费用超过了支付本身,会话就是正确的模式。 ### 代理使用场景 * **按 token 计费的 LLM 推理。** 客户端流式接收补全内容,并为每批 token 签署一个凭证;推理服务器在会话结束时结算。 * **按帧计费的视频。** 消费生成视频的代理为每 N 帧签署凭证;渲染器在流关闭时结算。 * **实时数据源。** 订阅者为预言机或市场数据流的每个 tick 付费,每个会话窗口结算一次。 ### 在 Stable 上的状态 会话需要两个 Stable 目前尚未提供的组件: 1. 一个面向 USDT0 的会话感知托管合约,用于持有存款并暴露 `settleVouchers`(或等效)函数。 2. 一个协调者,在卖方侧签发凭证并在买方侧进行验证,将提交批量发送给托管合约。 在两者都上线之前,MPP 会话在 Stable 上还无法使用。对于当今的高频代理支付,开销最低的模式是通过 Stable 的 [Gas 减免](/cn/how-to/integrate-gas-waiver) 提交的 **单次扣费** 意图,它消除了卖方侧的每笔交易 gas 成本,并使买方的 USDT0 余额成为唯一需要管理的资产。请参阅 [在 Stable 上构建 MPP 端点](/cn/how-to/build-mpp-endpoint) 了解每个请求的单次扣费模式。 ### 下一步推荐 * [**MPP 概念**](/cn/explanation/mpp) — 阅读更广泛的标准,包括单次扣费和订阅意图。 * [**代理结算**](/cn/explanation/agent-settlement) — 了解 MPP 会话在 Stable 代理支付通道中的位置。 * [**在 Stable 上构建 MPP 端点**](/cn/how-to/build-mpp-endpoint) — 在会话功能尚未到来之前,今天就使用单次扣费意图。 ## Machine Payments Protocol (MPP) MPP(Machine Payments Protocol,机器支付协议)是一个开放标准,用于在请求 HTTP 资源的同一请求中完成对该资源的付款。它扩展了 [x402](/cn/explanation/x402),新增了支付意图(charge、subscription、session)、多通道支持(稳定币、银行卡、Lightning)、生产特性(幂等性、正文摘要绑定、过期机制)以及额外的传输方式(MCP、WebSocket)。该协议处于 IETF 标准跟踪进程中。 ### MPP 与 x402 MPP 客户端向后兼容:MPP 客户端无需任何更改即可调用现有的 x402 服务器。两种协议的差异之处: | **方面** | **x402** | **MPP** | | :------------ | :------------------------------------------------------------ | :------------------------------------------------------- | | 支付意图 | 按请求收费 | Charge、subscription、session | | 通道 | 仅区块链 | 稳定币、银行卡、Lightning、自定义 | | 生产特性 | 有限 | 幂等性、正文摘要绑定、过期机制 | | 传输方式 | HTTP | HTTP、MCP/JSON-RPC、WebSocket | | 标头(客户端 ↔ 服务器) | `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` | `WWW-Authenticate` / `Authorization` / `Payment-Receipt` | | 治理 | 社区协议 | IETF 标准跟踪 | | 方法编写权 | 由基金会控制 | 无需许可 | ### 挑战、凭证、收据 MPP 将每个付费请求封装成客户端与资源服务器之间的三步交换: 1. **挑战(Challenge)。** 服务器返回 `402 Payment Required` 以及一个 `WWW-Authenticate` 标头,其中列出所支持的方法、金额和过期时间。 2. **凭证(Credential)。** 客户端选择一种方法,对支付凭证进行签名,并附带一个携带序列化凭证的 `Authorization` 标头重新提交请求。 3. **收据(Receipt)。** 服务器验证凭证、结算付款,并在响应中返回一个包含结算引用的 `Payment-Receipt` 标头。 挑战可以通过正文摘要在密码学上绑定到请求正文,因此为某个请求签名的凭证无法在另一个不同的请求上重用。 ### 支付意图 **Charge(收费)。** 针对单个资源的一次性付款。凭证授权一次精确金额的转账。 **Subscription(订阅)。** 在单个受限范围凭证下的周期性付款。凭证授权在一个计费周期内重复收费,由通道强制执行续订节奏。 **Session(会话)。** 使用链下凭单的按量付费。客户端先将资金存入托管一次,然后为每个请求签名低成本的链下凭单。只有净额在链上结算。详见 [MPP 会话](/cn/explanation/mpp-sessions)。 ### 传输方式 MPP 在多种传输方式上定义了相同的挑战 / 凭证 / 收据交换: * **HTTP。** 默认方式。标头如上所列。 * **MCP / JSON-RPC。** 让 MCP 服务器能够对单个工具调用进行变现。AI 客户端在调用工具前签名一份凭证。 * **WebSocket。** 持久连接,支持带内凭单充值,专为流式会话设计。 ### Stable 上的 MPP MPP 并未自带 Stable 支付方法。`mppx` SDK([wevm/mppx](https://github.com/wevm/mppx))包含针对 Tempo 和 Stripe 的方法,而 `mpp.dev` 列出了 Tempo、Stripe、Lightning、Solana、Stellar、Monad 和 RedotPay。目前这两个列表中都没有 Stable。 该标准无需许可,因此你可以编写自己的方法。对于 Stable 上的 USDT0,`verify()` 钩子是针对 ERC-3009 的签名验证,结算则委托给现有的结算服务:诸如 [Semantic Pay](https://docs.semanticpay.io) 或 [Heurist](https://docs.heurist.ai/x402-products/facilitator) 这样的 x402 facilitator,或 Stable 自有的 [Gas 豁免](/cn/how-to/integrate-gas-waiver)。完整演练请参阅 [在 Stable 上构建 MPP 端点](/cn/how-to/build-mpp-endpoint)。 ### 推荐后续 * [**在 Stable 上构建 MPP 端点**](/cn/how-to/build-mpp-endpoint) — 为 USDT0 编写三个 MPP 自定义方法钩子并结算一笔真实付款。 * [**MPP 会话**](/cn/explanation/mpp-sessions) — 使用链下凭单进行微支付流式传输,并一次性净额结算。 * [**x402**](/cn/explanation/x402) — 阅读 MPP 所泛化的原始 HTTP-402 协议。 ## 概览 Stable 是一条以 USDT0 作为原生 gas 代币的 Layer 1,标准的 EVM 工具链(Solidity、Foundry、Hardhat、ethers、viem 以及 `eth_*` JSON-RPC 方法)无需任何改动即可使用。 将你的 RPC 指向 Stable 并确认链 ID: ```text 988 ``` 确保你已安装 [foundry](https://www.getfoundry.sh/) 以测试以下命令: ```bash cast chain-id --rpc-url https://rpc.stable.xyz ``` 有关完整的端点列表(主网和测试网),请参阅 [连接](/cn/reference/connect)。 ### 接下来阅读什么 如果你还没有在 Stable 上发送过交易,请从 [快速开始](/cn/tutorial/quick-start) 入手,在测试网上快速体验一遍。然后根据你正在构建的内容选择对应的路径: * 钱包、委托和代理账户 → [账户](/cn/explanation/accounts-overview)。 * 转移 USDT0 或构建支付流程 → [支付](/cn/explanation/payments-overview)。 * 部署智能合约 → [合约](/cn/explanation/contracts-overview)。 * 接入 AI 编辑器或构建由代理付费的服务 → [代理结算](/cn/explanation/agent-settlement)。 * 运行全节点或归档节点、生态系统提供商或代付 gas → [基础设施](/cn/explanation/integrate-overview)。 在上线之前,[核心概念](/cn/explanation/core-concepts) 介绍了四个与以太坊不同的行为(USDT0 的双重角色、保证的区块空间、转账聚合器、EVM 最终性)。[生产就绪](/cn/how-to/production-readiness) 是主网就绪检查清单。 ## 用例概述 Stable 支持多种支付模式,从简单的钱包到钱包转账,到代理驱动的服务支付。下面的用例涵盖了当今可投入生产的模式。对于即将推出的模式(保证结算、隐私支付、代理间商务),请参阅[即将推出的用例](/cn/explanation/upcoming-use-cases)。 ### 实时用例 * [**P2P 支付**](/cn/reference/p2p-payments) — 钱包到钱包的 USDT0 转账。亚秒级结算,通过 Gas Waiver 实现零 gas。 * [**订阅计费**](/cn/reference/subscriptions) — 通过 EIP-7702 实现的拉取式定期计费。订阅者授权一次,提供商每个周期收款。 * [**发票结算**](/cn/reference/invoices) — 使用确定性 nonce 的 B2B 发票支付。链上结算自动关联到发票。 * [**按调用付费 API**](/cn/reference/pay-per-call) — 通过 x402 中间件实现的按请求 HTTP 支付。无需账户、无需 API 密钥、无需计费周期。 ### 共享基础 大多数模式都构建在相同的两个协议之上: * **[ERC-3009](/cn/explanation/erc-3009)**:用于委托结算的签名授权。被发票、按调用付费和 P2P 应用发起的转账使用。 * **[x402](/cn/explanation/x402)**:基于标准请求头的 HTTP 原生支付。被按调用付费 API 和 MCP 驱动的支付流程使用。 * **[EIP-7702](/cn/explanation/eip-7702)**:用于定期授权的 EOA 委托。被订阅计费使用。 ### 下一步推荐 * [**ERC-3009**](/cn/explanation/erc-3009) — 从核心结算标准开始。 * [**即将推出的用例**](/cn/explanation/upcoming-use-cases) — 预览代理间商务、保证结算和隐私支付。 ## 支付指南 “支付”标签下的每个指南、概念和参考,按你想要完成的任务分组。 ### 发送和转账 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 在同一余额上进行原生和 ERC-20 转账。 * [**零 Gas 交易**](/cn/how-to/zero-gas-transactions) — 通过 Gas Waiver 支付费用来转账 USDT0。 * [**将 USDT0 用作 Gas**](/cn/how-to/work-with-usdt-gas) — 正确构造交易:优先小费为 0,`value` 以 USDT0 计。 * [**将 USDT0 桥接到 Stable**](/cn/tutorial/bridge-usdt0) — 使用 LayerZero OFT 从 Ethereum Sepolia 桥接。 ### 构建支付流程 * [**了解 P2P 支付**](/cn/how-to/build-p2p-payments) — 在一个应用中实现钱包 + 发送 + 接收 + 历史记录。 * [**订阅和收款**](/cn/how-to/subscribe-and-collect) — 通过 EIP-7702 实现基于拉取的循环计费。 * [**使用发票付款**](/cn/how-to/pay-with-invoice) — 使用带确定性 nonce 的 ERC-3009 进行发票结算。 * [**构建按次付费 API**](/cn/how-to/build-pay-per-call) — 使用 x402 中间件将 HTTP 端点货币化。 ### 协议和参考 * [**ERC-3009**](/cn/explanation/erc-3009) — Transfer With Authorization:签名结算原语。 * [**x402(HTTP 原生支付)**](/cn/explanation/x402) — 服务器返回 402,客户端签名,facilitator 在链上结算。 * [**P2P 支付参考**](/cn/reference/p2p-payments) — 模型概述以及与传统支付通道的比较。 * [**订阅参考**](/cn/reference/subscriptions) — 基于拉取的计费模型及其权衡。 * [**发票参考**](/cn/reference/invoices) — 确定性 nonce 结算模型。 * [**按次付费参考**](/cn/reference/pay-per-call) — x402 定价和端点发现模型。 * [**即将推出的用例**](/cn/explanation/upcoming-use-cases) — 保证结算、机密支付、代理对代理。 ## Stable 上的支付 Stable 是围绕支付构建的。USDT0 是原生资产,也是 gas 代币,因此结算和手续费共享同一个余额。单槽终局性(single-slot finality)意味着一笔转账可在一秒内完成。ERC-3009、EIP-7702 和 x402 都是原生基元,而非变通方案——你可以用一个签名完成结算、从委托账户中拉取资金,或者无需运行计费系统即可按 HTTP 请求收费。 ### 你可以构建什么 * **P2P 转账** — 原生 USDT0 转账,21k gas,亚秒级终局性。 * **订阅** — 基于拉取的周期性计费,使用 EIP-7702 委托。 * **发票结算** — 使用带确定性 nonce 的 ERC-3009 `transferWithAuthorization`,实现精确对账。 * **按次调用 API** — 使用 x402 中间件实现按请求的 USDT0 支付;无需 API 密钥,无需注册。 * **零 gas 用户体验** — 通过 Gas Waiver 服务实现应用赞助的交易。 * **跨链 USDT0** — 通过 LayerZero OFT 从以太坊及其他网络进行桥接。 ### Stable 的不同之处 * **一种资产搞定一切**:发送方无需持有单独的 gas 代币。 * **原生 ERC-3009**:USDT0 直接实现了 `transferWithAuthorization`,因此支付只需一个签名即可结算,无需 approve 步骤。 * **确定性终局**:区块在提交的那一刻即为最终状态。无需等待确认。 * **原生 x402**:facilitator 通过 Gas Waiver 不支付 gas,因此每次请求的结算成本可保持在一美分以下。 ### 从这里开始 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 在同一余额上进行原生和 ERC-20 转账。 * [**零 gas 交易**](/cn/how-to/zero-gas-transactions) — 通过 Gas Waiver 支付手续费来转账 USDT0。 * [**学习 P2P 支付**](/cn/how-to/build-p2p-payments) — 从零构建一个钱包 + 发送 + 接收 + 历史记录的应用。 * [**构建按次调用 API**](/cn/how-to/build-pay-per-call) — 使用 x402 为 HTTP 端点按请求定价。 * [**Stable SDK**](/cn/explanation/sdk-overview) — 使用类型化客户端,几行代码即可实现转账、桥接和兑换。 ### 支付基元 * [**ERC-3009**](/cn/explanation/erc-3009) — Transfer With Authorization:发票和 x402 背后的结算标准。 * [**x402(HTTP 原生支付)**](/cn/explanation/x402) — 服务器响应 402,客户端签署 ERC-3009,facilitator 在链上结算。 ### 接下来推荐 * [**支付指南索引**](/cn/explanation/payments-guides) — 支付标签下的所有指南、概念和参考资料。 * [**订阅与收款**](/cn/how-to/subscribe-and-collect) — 通过 EIP-7702 实现基于拉取的周期性计费。 * [**使用发票付款**](/cn/how-to/pay-with-invoice) — 使用带确定性 nonce 的 ERC-3009 实现精确对账。 ## Stable SDK `@stablechain/sdk` 是 Stable 的官方 TypeScript 客户端。它对 viem 进行了封装,为你最常用的操作提供了精简的类型化 API:转账 USDT0、在链之间桥接以及在 Stable 上兑换代币。路由、授权、小数位数和链切换都已为你处理好。 ```ts import { createStable, Network } from "@stablechain/sdk"; import { privateKeyToAccount } from "viem/accounts"; const stable = createStable({ network: Network.Mainnet, account: privateKeyToAccount("0x..."), }); const { txHash } = await stable.transfer({ from: "0xYourAddress", to: "0xRecipient", amount: 10, }); ``` ```text txHash: 0x8f3a...2d41 ``` ### SDK 的功能 * **`transfer`** — 在 Stable 上发送原生 USDT0 或任意 ERC-20 代币。Gas 会自动以 USDT0 支付。 * **`quoteBridge` / `bridge`** — 跨链转账。USDT0 → USDT0 使用 LayerZero,其他情况使用 LI.FI。路由会自动为你选择。 * **`quoteSwap` / `swap`** — 通过 LI.FI 进行同链代币兑换,内部已处理 ERC-20 授权。 该 SDK 以 [`@stablechain/sdk`](https://www.npmjs.com/package/@stablechain/sdk) 发布在 npm 上,并需要 `viem >= 2.0.0` 作为对等依赖。 ### 何时使用(以及何时不使用) 当你想要一个类型化的、有既定约定的客户端来隐藏路由和授权样板代码时,请使用该 SDK。当你需要直接控制交易构造、自定义 gas 策略,或进行 transfer / bridge / swap 之外的合约调用时,请使用原生 viem 或 ethers。 :::note 该 SDK 支持使用任何兼容 viem 的签名器进行签名:私钥 `Account`、像 `custom(window.ethereum)` 这样的浏览器 `Transport`,或者预先构建的 `WalletClient`(例如 wagmi 的 `useWalletClient` 返回的那个)。 ::: ### 从这里开始 * [**快速开始**](/cn/tutorial/sdk-quickstart) — 安装 SDK 并在测试网上运行你的第一次转账、桥接和兑换。 * [**SDK 参考**](/cn/reference/sdk) — 每个方法、配置选项、枚举和错误类。 * [**与 viem 一起使用**](/cn/how-to/sdk-with-viem) — 服务端账户、浏览器钱包,以及自带的 `WalletClient`。 * [**与 wagmi 一起使用**](/cn/how-to/sdk-with-wagmi) — 使用 `useWalletClient` 和 hooks 将 SDK 接入 React 应用。 ### 推荐的下一步 * [**从 npm 安装**](https://www.npmjs.com/package/@stablechain/sdk) — 在 npmjs.com 上查看该包并检查最新版本。 * [**连接到 Stable**](/cn/reference/connect) — 主网和测试网的链 ID、RPC 端点和浏览器。 * [**为测试网钱包充值**](/cn/how-to/use-faucet) — 在运行快速开始之前,从水龙头获取测试网 USDT0。 ## StableDB 区块链端到端性能的主要瓶颈之一是 **磁盘 I/O(Disk I/O)**。尤其是在区块执行后提交和存储状态数据的操作,是性能的关键瓶颈。Stable 通过架构创新,使用如 `MemDB`、`VersionDB` 以及文件内存映射存储(`mmap`),显著提升系统吞吐量。 ### 为什么磁盘 I/O 是性能瓶颈 #### 状态转换与持久化 每当执行一批交易并产出区块时,区块链系统都会从一个状态转变为下一个状态。这个过程分为两个基本阶段: 1. **状态提交(State Commitment)**:在交易执行完成后,提交新的状态。 2. **状态存储(State Storage)**:已提交的状态被持久化到磁盘,用于长期访问和历史验证。 状态提交与存储耦合 在传统架构中,状态存储与状态提交是**紧密耦合**的,这意味着: * 节点在继续执行下一个区块之前,必须等待新状态完全写入磁盘。 * 状态数据被写入磁盘中随机的位置,这导致在后续交易执行中读取状态时出现高延迟。 即使共识层和执行层做了大量优化,这种对低效的串行磁盘操作的依赖仍然限制了整个系统的性能上限。 ### 针对高吞吐量的数据库优化 为了解决这些限制,Stable 提出了两项核心架构优化:**解耦状态操作** 和 **引入内存映射数据库优化(mmap)**。 #### 1. 拆解状态提交与存储 状态提交与存储拆解 第一步是将状态提交与其存储过程拆解: * 在提交新状态后,节点可以立即执行下一个区块。 * 状态的持久化操作在后台异步进行。 这种分离让执行可以得到立即的处理,绕过磁盘写入带来的延迟,从而消除阻塞依赖,大幅提升端到端性能。 #### 2. 基于 `mmap` 的 `MemDB` 和 `VersionDB` 进一步通过 `mmap`(内存映射文件)实现双数据库模型: * **MemDB(内存数据库)**: * 存储近期频繁访问的活跃状态。 * 使用固定地址映射(通过 `mmap`),支持快速的数据查找。 * 适用于大多数以近期状态为目标的交易场景。 * **VersionDB(历史数据库)**: * 存储更早期的历史状态。 * 优化用于归档和长周期查询,针对低频访问设计。 这种设计确保**热点数据通过内存驻留结构快速响应**,而冷数据则由较慢的持久化存储承担。通过结合 `mmap` 访问与状态智能分层,Stable 能够显著降低区块执行过程中的数据库读写延迟。 ### 预期收益与已有实践 这种架构优化并非理论设想,已有 Sei 和 Cronos 等高性能区块链采用类似的解耦式内存映射数据库架构,实测可带来 \*\* 高达 2 倍的整体 TPS 性能提升\*\*。 Stable 也预期将获得类似的性能提升,因为该架构不再受限于存储层瓶颈,系统的共识与执行性能可以按需扩展,而不会被磁盘操作拖慢。 ### 延伸阅读 如需深入了解相关技术与实现细节,请参阅: * [ADR-065:Cosmos Store V2 架构](https://docs.cosmos.network/main/build/architecture/adr-065-store-v2) * [MemIAVL:实用指南](https://hackmd.io/@yihuang/rkeCvy5xh) * [Cronos MemIAVL 节点配置](https://docs.cronos.org/for-node-hosts/running-nodes/memiavl) * [Sei 的数据库设计方案](https://4pillars.io/ko/articles/sei-db) ## 质押模块 ### 概述 `staking` 预编译合约作为桥梁,使 Stable SDK 的 `x/staking` 模块功能能在 EVM 环境中使用。 ### 目录 1. **[概念](#concepts)** 2. **[配置](#configuration)** 3. **[方法](#methods)** 4. **[事件](#events)** ### 概念 在 Stable SDK 的 `x/staking` 模块中,必须在链初始化时注册绑定denomination以进行质押。 验证者和委托者只能使用绑定denomination质押代币。 在 `staking` 预编译合约中,会进行额外检查以确保验证者或委托者是调用者。 ### 配置 合约地址和gas费用已预定义。 #### 合约地址 * `0x0000000000000000000000000000000000000800` ### 方法 #### `createValidator` 创建一个验证者。 验证者必须以来自操作员的初始委托创建。 对于潜在的委托者,验证者应提供他们的信息和佣金率计划。 委托者可以通过公开信息和市场机制的自然调节来选择验证者委托他们自己的代币。 当验证者成功注册时,会发出 `CreateValidator` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ----------------- | --------------- | ---------------- | | description | Description | 验证者的信息 | | commissionRates | CommissionRates | 验证者奖励质押代币的佣金率 | | minSelfDelegation | uint256 | 验证者的最小自委托金额 | | validatorAddress | address | 验证者的地址 | | pubkey | string | 验证者的公钥 | | value | uint256 | 最初自委托给验证者的质押代币数量 | `Description` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | --------------- | ------ | --------- | | moniker | string | 验证者的名称 | | identity | string | 验证者的身份 | | website | string | 验证者网站的URL | | securityContact | string | 安全联系信息 | | details | string | 验证者的额外描述 | `CommissionRates` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ------------- | ------- | ---------------- | | rate | uint256 | 验证者获得的当前佣金率 | | maxRate | uint256 | 最大佣金率(不能设置得比这更高) | | maxChangeRate | uint256 | 验证者一天内可以更改的最大佣金率 | `rate` 应设置为市场可接受的适当值。 * 如果验证者的佣金率较高,委托者的利润就较低。 * 如果验证者的佣金率较低,验证者的利润就较低,这使得运营变得困难。 由于高 `maxRate` 会让委托者担心验证者意外设置高佣金率,因此应小心设置 `maxRate`。`maxChangeRate` 在初始化后不可更改。 ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果验证者成功注册,则为true | #### `editValidator` 验证者更新其信息。 验证者只能更新除了 `CommissionRates` 结构中不可更改字段(如 `maxRate` 和 `maxChangeRate`)之外的信息。 当验证者成功更新时,会发出 `EditValidator` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ----------------- | ----------- | ------------- | | description | Description | 验证者的信息 | | validatorAddress | address | 验证者的地址 | | commissionRate | int256 | 验证者奖励质押代币的佣金率 | | minSelfDelegation | int256 | 验证者的最小自委托金额 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | ---------------- | | success | bool | 如果验证者成功更新,则为true | #### `delegate` 委托者设置要委托给验证者的代币数量。 当委托成功完成时,会发出 `Delegate` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------------- | | delegatorAddress | address | 委托者的地址 | | validatorAddress | address | 验证者的地址 | | amount | uint256 | 委托给验证者的质押代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | --------------- | | success | bool | 如果委托成功完成,则为true | ##### 事件 `newShares` 表示委托者的所有权比例。 即使委托相同数量的代币,计算出的份额也可能因时间而异。 #### `undelegate` 委托者提取委托给验证者的代币数量。 当取消委托成功完成时,会发出 `Unbond` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------------------- | | delegatorAddress | address | 委托者的地址 | | validatorAddress | address | 验证者的地址 | | amount | uint256 | 愿意从验证者那里取消委托的质押代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | ----------------- | | success | bool | 如果取消委托成功完成,则为true | #### `redelegate` 委托者将委托给验证者的代币数量重新委托给另一个验证者。 当重新委托成功完成时,会发出 `Redelegate` 事件。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ----------- | | delegatorAddress | address | 委托者的地址 | | validatorSrc | string | 源验证者的地址 | | validatorDst | string | 目标验证者的地址 | | amount | uint256 | 重新委托的质押代币数量 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ---- | ----------------- | | success | bool | 如果重新委托成功完成,则为true | #### `delegation` 返回委托者和验证者之间的委托信息。 如果未找到委托,`shares` 和 `balance` 将为 `0`。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | delegatorAddress | address | 委托者的地址 | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------- | ------- | -------------------- | | shares | uint256 | 委托的份额 | | balance | Coin | 委托代币的数量和denomination | `Coin` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ------ | ------- | --------------- | | denom | string | 奖励的denomination | | amount | uint256 | 奖励的数量 | #### `unbondingDelegation` 返回委托者和验证者之间的解绑委托信息。 如果未找到解绑委托,将返回空的 `UnbondingDelegationOutput`。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | delegatorAddress | address | 委托者的地址 | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------------------- | ------------------------- | ------- | | unbondingDelegation | UnbondingDelegationOutput | 解绑委托的信息 | `UnbondingDelegationOutput` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ---------------- | --------------------------- | ------- | | validatorAddress | address | 验证者的地址 | | delegatorAddress | address | 委托者的地址 | | entries | UnbondingDelegationEntry\[] | 解绑委托的条目 | `UnbondingDelegationEntry` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | -------------- | ------ | ------- | | creationHeight | uint64 | 条目的创建高度 | | completionTime | uint64 | 条目的完成时间 | | initialBalance | Coin | 条目的初始余额 | | balance | Coin | 条目的余额 | #### `validator` 返回验证者信息。 如果未找到验证者,将返回空的 `ValidatorOutput`。 ##### 输入参数 | 名称 | 类型 | 描述 | | ---------------- | ------- | ------ | | validatorAddress | address | 验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | --------- | --------- | ------ | | validator | Validator | 验证者的信息 | `Validator` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ----------------- | ------- | ------------- | | operatorAddress | address | 验证者的地址 | | consensusPubkey | string | 验证者的公钥 | | jailed | bool | 验证者是否被监禁 | | status | int32 | 验证者的状态 | | tokens | uint256 | 委托给验证者的质押代币数量 | | delegatorShares | uint256 | 委托份额的数量 | | description | string | 验证者的描述 | | unbondingHeight | int64 | 验证者解绑的高度 | | unbondingTime | int64 | 验证者解绑的时间 | | commission | uint256 | 验证者奖励质押代币的佣金率 | | minSelfDelegation | uint256 | 验证者的最小自委托金额 | #### `validators` 返回所有与状态匹配的验证者。 如果未找到验证者,将返回空的 `ValidatorsOutput`。 状态在 `x/staking` 模块中声明,可以是以下之一: * 0 : "BOND\_STATUS\_UNSPECIFIED", 未指定状态 * 1 : "BOND\_STATUS\_UNBONDING", 验证者正在解绑 * 2 : "BOND\_STATUS\_UNBONDED", 验证者已解绑 * 3 : "BOND\_STATUS\_BONDED", 验证者已绑定 ##### 输入参数 | 名称 | 类型 | 描述 | | ----------- | ------- | ------ | | status | string | 验证者的状态 | | pageRequest | PageReq | 分页请求 | `PageReq` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ---------- | ----- | -------- | | key | bytes | 页面的键 | | offset | int64 | 页面的偏移量 | | limit | int64 | 页面的限制 | | countTotal | bool | 是否计算结果总数 | | reverse | bool | 是否反转结果 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------------ | ------------ | ----- | | validators | Validator\[] | 验证者数组 | | pageResponse | PageResp | 分页响应 | `PageResp` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ------- | ------ | ------- | | nextKey | bytes | 页面的下一个键 | | total | uint64 | 结果的总数 | #### `redelegation` 返回委托者、源验证者和目标验证者的重新委托信息。 如果未找到重新委托,将返回空的 `RedelegationOutput`。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------------------- | ------- | -------- | | delegatorAddress | address | 委托者的地址 | | srcValidatorAddress | address | 源验证者的地址 | | dstValidatorAddress | address | 目标验证者的地址 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------------ | ------------------ | ------- | | redelegation | RedelegationOutput | 重新委托的信息 | `RedelegationOutput` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | ------------------- | -------------------- | -------- | | delegatorAddress | address | 委托者的地址 | | validatorSrcAddress | address | 源验证者的地址 | | validatorDstAddress | address | 目标验证者的地址 | | entries | RedelegationEntry\[] | 重新委托的条目 | `RedelegationEntry` 是一个包含以下字段的结构: | 名称 | 类型 | 描述 | | -------------- | ------ | ------- | | creationHeight | uint64 | 条目的创建高度 | | completionTime | uint64 | 条目的完成时间 | | initialBalance | Coin | 条目的初始余额 | | balance | Coin | 条目的余额 | #### `redelegations` 返回委托者、源验证者和目标验证者的所有重新委托。 如果未找到重新委托,将返回空的 `RedelegationResponse` 和 `PageResp`。 ##### 输入参数 | 名称 | 类型 | 描述 | | ------------------- | ------- | -------- | | delegatorAddress | address | 委托者的地址 | | srcValidatorAddress | address | 源验证者的地址 | | dstValidatorAddress | address | 目标验证者的地址 | | pageRequest | PageReq | 分页请求 | ##### 输出参数 | 名称 | 类型 | 描述 | | ------------ | ----------------------- | ------- | | response | RedelegationResponse\[] | 重新委托的信息 | | pageResponse | PageResp | 分页响应 | ### 事件 #### CreateValidator | 名称 | 类型 | 索引 | 描述 | | -------- | ------- | -- | ---------------- | | valiAddr | address | Y | 验证者的地址 | | value | uint256 | N | 最初自委托给验证者的质押代币数量 | #### EditValidator | 名称 | 类型 | 索引 | 描述 | | ----------------- | ------- | -- | --------------- | | valiAddr | address | Y | 验证者的地址 | | commissionRate | int256 | N | 验证者奖励质押代币的更新佣金率 | | minSelfDelegation | int256 | N | 验证者的更新最小自委托金额 | #### Delegate | 名称 | 类型 | 索引 | 描述 | | ------------- | ------- | -- | ------------- | | delegatorAddr | address | Y | 委托者的地址 | | validatorAddr | string | Y | 验证者的地址 | | amount | uint256 | N | 委托给验证者的质押代币数量 | | newShares | uint256 | N | 委托后的委托份额数量 | #### Unbond | 名称 | 类型 | 索引 | 描述 | | -------------- | ------- | -- | --------------- | | delegatorAddr | address | Y | 委托者的地址 | | validatorAddr | string | Y | 验证者的地址 | | amount | uint256 | N | 从验证者取消委托的质押代币数量 | | completionTime | uint256 | N | 取消委托的完成时间 | #### Redelegate | 名称 | 类型 | 索引 | 描述 | | ------------------- | ------- | -- | ----------- | | delegatorAddr | address | Y | 委托者的地址 | | validatorSrcAddress | address | Y | 源验证者的地址 | | validatorDstAddress | address | Y | 目标验证者的地址 | | amount | uint256 | N | 重新委托的质押代币数量 | | completionTime | uint256 | N | 重新委托的完成时间 | ## 系统模块 Stable 的核心协议行为位于 SDK 模块中:`x/bank`、`x/distribution`、`x/staking`。为了让这些行为能从 EVM 访问,Stable 将每个模块作为固定地址上的**预编译合约**暴露出来。用 Solidity 编写的合约直接调用预编译,EVM 会将调用路由到原生 SDK 处理程序。预编译在协议层面实现,使其比等效的 Solidity 重新实现显著更省 gas。 ### 三个模块 | 模块 | 预编译地址 | 用途 | | :-------------------------------------------------- | :--------------------- | :----------------------------- | | [Bank](/cn/explanation/bank-module) | `0x0000…1003` (STABLE) | 代币转账、余额记账、授权管理、为授权合约执行铸造/销毁。 | | [Distribution](/cn/explanation/distribution-module) | `0x0000…0801` | 质押奖励领取、奖励查询、提取地址管理。 | | [Staking](/cn/explanation/staking-module) | `0x0000…0800` | 委托、解除委托、重新委托、验证人查询。 | | [系统交易](/cn/explanation/system-transactions) | `0x0000…9999` | 协议为 SDK 层操作(例如解绑完成)发出的 EVM 事件。 | 上面的每个页面都解释了该模块的功能、何时使用以及在哪里找到它的 ABI。 ### 为什么用预编译,而不是 Solidity 两个原因: * **Gas 效率。** 预编译运行在协议的原生执行路径中。等效的 Solidity 合约会以显著更高的 gas 成本重新实现相同的逻辑。 * **单一可信来源。** 质押、分配和代币供应是协议层面的状态。通过预编译暴露这些状态,避免维护一个可能与 SDK 产生偏差的重复 Solidity 实现。 ### 授权 某些预编译方法(`mint`、`burn`、协议层面的质押操作)需要调用方授权。`x/precompile` 模块维护着一个链上白名单,来自未注册合约的调用会被回退。这使得特权操作受治理门控,同时不会阻碍 EVM 对只读/转账方法的常规使用。 ### 推荐后续阅读 * [**Bank 模块**](/cn/explanation/bank-module) — 了解代币转账、授权以及铸造/销毁授权模型。 * [**Staking 模块**](/cn/explanation/staking-module) — 了解委托和验证人管理如何到达 EVM。 * [**系统交易**](/cn/explanation/system-transactions) — 了解解绑完成等协议层面的事件如何以 EVM 日志的形式呈现。 ## System Transactions ### 摘要 系统交易为 Stable 协议提供了一种为 Stable SDK 操作发出 EVM 事件的方式。当质押事件(如解绑完成)在 SDK 层发生时,协议会自动生成发出相应事件的 EVM 交易,使这些操作对 EVM 工具和应用程序完全可见。 ### 动机 Stable 上的 EVM 用户和应用程序期望通过标准 EVM 接口(如 `eth_getLogs`)监控区块链事件。但关键操作发生在 Stable SDK 模块中,这些模块不会自然地发出 EVM 事件。这造成了可见性差距:EVM dapps 无法轻松跟踪用户的代币何时完成解绑。 系统交易弥合了这一差距。当质押模块完成解绑操作时,Stable 的 x/stable 模块会检测到该事件并生成一个调用 StableSystem 预编译合约(`0x0000000000000000000000000000000000009999`)的系统交易。然后,预编译合约会发出任何 dapp 都可以订阅的适当 EVM 事件。系统交易使用特殊的发送者地址(`0x8888888888888888888888888888888888888888`)运行,只有协议才能使用该地址。这可以防止任何人伪造协议事件,同时保持事件发出在链上的无需信任和可验证性。 ### 规范 系统交易通过三个主要组件工作:x/stable 模块的 EndBlocker、PrepareProposal 处理程序和 StableSystem 预编译合约。 #### 架构概述 system-transaction-architecture #### StableSystem 预编译合约 StableSystem 预编译合约位于 `0x0000000000000000000000000000000000009999`,处理需要发出 EVM 事件的协议级操作。目前它支持解绑完成通知。 ```solidity interface IStableSystem { /// @notice 处理排队的解绑完成并发出 EVM 事件 /// @param blockHeight 处理完成的区块高度 /// @dev 只能由系统交易调用(from = 0x8888888888888888888888888888888888888888) /// @dev 每次调用最多处理 100 个完成 /// @dev 自动从队列中删除已处理的完成 function notifyUnbondingCompletions(int64 blockHeight) external; /// @notice 当解绑操作完成时发出 /// @param delegator 委托代币的地址 /// @param validator 代币委托给的验证者地址 /// @param amount 完成解绑的代币数量(以 uusdc 为单位) event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); /// @notice 调用者未授权(不是系统交易发送者) error Unauthorized(); } ``` #### 系统交易发送者 系统交易使用 `0x8888888888888888888888888888888888888888` 作为发送者地址。该地址: * 不需要签名验证 * 只能由 PrepareProposal 中创建的交易使用 * 用户或合约无法伪造 * 通过 SystemTxDecorator ante 处理程序跳过费用扣除 EVM 通过检查 `msg.sender == 0x8888888888888888888888888888888888888888` 来识别系统交易。预编译合约可以使用此功能来限制仅协议操作。 #### 事件驱动流程 当用户的解绑期完成时,会发生以下情况: 1. **Stable SDK 层:** 质押模块的 EndBlocker 完成解绑并发出 EventTypeCompleteUnbonding,包含委托者地址、验证者地址和金额。 2. **检测:** x/stable 模块的 EndBlocker 在质押之后运行,并扫描区块事件日志中的解绑事件。每当有代币完成解绑,该模块都将在队列中添加一条记录,包含委托者地址、验证者地址、金额和区块高度。 3. **系统交易生成**:在下一个区块的 PrepareProposal 中,应用程序查询所有排队的完成。如果存在任何完成,它会创建一个调用 StableSystem.notifyUnbondingCompletions(blockHeight) 的系统交易,使用当前区块高度。此交易放在区块前面,在任何用户交易之前。 4. **执行:** 在区块执行期间,系统交易首先运行。预编译合约查询该区块高度排队的完成状态,为每个完成发出一个 UnbondingCompleted 事件(最多 100 个),并从队列中删除它们。 5. **EVM 可见性:** 事件出现在交易收据和日志中,对 eth\_getLogs 查询、区块浏览器和任何监控 StableSystem 预编译合约的应用程序可见。 #### 批处理 为防止区块变得过大,系统每个区块最多处理 100 个解绑完成。如果队列中存在 150 条记录: * 区块 N:创建处理完成 0-99 的系统交易 * 区块 N+1:创建处理完成 100-149 的系统交易 预编译合约直接查询状态,而不是在 calldata 中接收完成数据。这使交易大小可预测,并将数据从昂贵的 calldata 移动到更便宜的状态读取。 ### 使用示例 最常见的用例是需要在解绑期完成时通知用户的质押仪表板。以下是如何设置解绑完成监听器。 ```javascript import { ethers } from 'ethers'; // StableSystem 预编译合约地址 const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999'; // UnbondingCompleted 事件的 ABI const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; // 连接到 Stable 网络 const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz'); const stableSystem = new ethers.Contract( STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI, provider ); // 订阅所有解绑完成 stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => { console.log('解绑完成!'); console.log('委托者:', delegator); console.log('验证者:', validator); console.log('金额:', ethers.formatEther(amount), '代币'); console.log('区块:', event.log.blockNumber); console.log('交易哈希:', event.log.transactionHash); }); ``` 此监听器将在任何用户的解绑完成时触发。在生产环境中部署 dApp 时需要过滤特定用户的事件。 #### 过滤特定用户的事件 要仅接收特定委托者地址的事件,请使用索引事件参数创建过滤器: ```javascript // 仅监视特定用户的解绑 const userAddress = '0xabcd...'; const filter = stableSystem.filters.UnbondingCompleted(userAddress); stableSystem.on(filter, (delegator, validator, amount, event) => { // 这仅针对指定用户的解绑触发 showNotification(`您的 ${ethers.formatEther(amount)} 代币解绑完成!`); refreshUserBalance(userAddress); }); ``` 如果您正在构建特定于验证者的仪表板,您还可以按验证者过滤: ```javascript // 监视来自特定验证者的所有解绑 const validatorAddress = '0x1234...'; const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress); stableSystem.on(validatorFilter, (delegator, validator, amount) => { updateValidatorStats(validator, amount); }); ``` #### 查询历史事件 如果您的 dApp 需要显示过去解绑完成的历史记录,您可以使用带有区块范围的事件过滤器查询历史事件: ```javascript // 获取用户在最近 1000 个区块中的所有解绑 const currentBlock = await provider.getBlockNumber(); const filter = stableSystem.filters.UnbondingCompleted(userAddress); const events = await stableSystem.queryFilter( filter, currentBlock - 1000, currentBlock ); const unbondingHistory = events.map(event => ({ delegator: event.args.delegator, validator: event.args.validator, amount: ethers.formatEther(event.args.amount), blockNumber: event.blockNumber, txHash: event.transactionHash })); console.log('最近的解绑:', unbondingHistory); ``` ### 集成指南 #### 步骤 1:添加 Stable System 合约接口 首先,将 StableSystem 预编译合约接口添加到您的项目中。如果您使用 Foundry 或 Hardhat,请创建一个新的接口文件: ```solidity interface IStableSystem { event UnbondingCompleted( address indexed delegator, address indexed validator, uint256 amount ); } ``` 如果您正在构建一个没有 Solidity 合约的纯前端 dApp,您只需要事件的 ABI 片段: ```javascript const STABLE_SYSTEM_ABI = [ 'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)' ]; ``` #### 步骤 2:设置事件监听器 初始化您的 ethers.js provider 并创建指向 StableSystem 预编译合约地址的合约实例。预编译合约始终部署在 Stable 测试网和主网的 `0x00000000000....0000009999`。 *注意:预编译合约尚未部署在 Stable 主网上,将在 v1.2.0 升级后提供。* ```javascript const provider = new ethers.JsonRpcProvider(RPC_URL); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); ``` #### 步骤 3:在应用程序逻辑中处理事件 订阅事件并相应地更新应用程序状态。常见模式包括: * **余额更新**:当解绑完成时,刷新用户的代币余额 * **通知系统**:在用户的解绑完成时显示 toast 通知 * **仪表板统计**:实时更新质押指标和图表 * **交易历史**:将已完成的解绑添加到用户的活动源 #### 步骤 4:处理连接问题 由于事件订阅依赖于持久的 websocket 连接,因此为生产 dApp 实现重新连接逻辑: ```javascript let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; function setupEventListener() { const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz'); provider.on('error', (error) => { console.error('Provider 错误:', error); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => setupEventListener(), 5000); } }); const stableSystem = new ethers.Contract( '0x0000000000000000000000000000000000009999', STABLE_SYSTEM_ABI, provider ); stableSystem.on('UnbondingCompleted', handleUnbonding); } ``` ### 为什么采用这种方法? #### 与自定义索引器相比 以前,Stable SDK 要求 dApp 开发人员运行自定义索引器,监视 SDK 事件并将它们存储在数据库中。这增加了操作开销并引入了潜在的故障点。 使用系统交易,无需单独的索引器基础设施。EVM 的日志系统原生支持这类事件,每个 RPC 节点都已经索引和提供。任何标准 web3 库都可以订阅这些事件,无需额外工具。 #### 与轮询 SDK 端点相比 没有系统交易,EVM dApps 需要定期调用 Stable SDK REST 端点来检查解绑期是否已完成。这会产生几个问题: * **延迟增加**:5-10 秒的轮询间隔意味着用户可能需要等待那么长时间才能看到更新 * **更高的负载**:每个 dApp 实例轮询端点都会增加 RPC 基础设施的负载 * **复杂性**:dApps 需要同时处理 web3 提供程序(用于 EVM 交互)和 Stable SDK REST 客户端(用于 SDK 查询) * **无实时更新**:轮询本质上无法提供即时通知 系统交易通过 dApps 已经用于 EVM 交互的相同 websocket 连接提供实时事件通知。这简化了开发人员体验并降低了基础设施成本。 ### 安全保证 #### 无需信任的事件发出 系统交易在 `PrepareProposal` ABCI 阶段创建,只有验证者才能执行。用户提交的交易无法伪造系统发送者地址(`0x8888888888888888888888888888888888888888`),因为 EVM 的状态转换逻辑强制只有到 StableSystem 预编译合约地址的交易才能跳过签名验证。 这意味着: * 用户无法伪造解绑完成事件 * 用户无法从自己的交易中调用 `notifyUnbondingCompletions` * 发出 `UnbondingCompleted` 事件的唯一方法是在 Stable SDK 质押模块中实际完成解绑 #### 无额外信任假设 系统交易不会引入超出区块链共识已经需要的新安全假设。如果您相信验证者正确执行区块,您就可以相信系统交易事件准确反映了 Stable SDK 状态变化。 事件发出过程是确定性的:给定 `EndBlock` 中相同的 SDK 事件,所有诚实的验证者将在 `PrepareProposal` 期间产生相同的系统交易。共识机制确保验证者就包含哪些系统交易达成一致。 #### 区块最终性 Stable 区块链通过 StableBFT 的共识机制使用快速最终性。一旦提交了一个区块,它就会立即最终化,无法重组。这意味着一旦您收到 `UnbondingCompleted` 事件,您就可以相信它是永久的。 不需要像在概率最终性链上那样等待多个确认。dApps 可以在收到事件后立即更新用户余额并显示通知。 ### 性能和限制 #### 批处理大小约束 每个区块通过系统交易最多处理 100 个解绑完成。此限制存在是为了防止在解绑活动高峰期间区块大小无限制。 在实践中,假设平均区块时间为 0.7 秒,每个区块 100 个完成提供了约 9000 个完成/分钟的吞吐量。正常的质押活动很少达到此限制。在特殊情况下,完成可能会在完全处理之前排队几个区块。 #### Gas 消耗 系统交易在执行期间消耗 gas,这在区块的 gas 限制中计算。gas 成本与正在处理的完成数量成线性比例: * 基本函数调用:约 21,000 gas * 每个事件发出:约 3,000 gas * 读取状态:每个完成约 2,000 gas 100 个完成的完整批次消耗大约 521,000 gas。由于 Stable 的区块 gas 限制为 100,000,000,这代表不到 0.6% 的可用区块空间。 #### 通知延迟 当解绑期在区块 N 期间完成时: 1. Stable 模块的 `EndBlock` 在区块 N 的状态中排队完成 2. 区块 N+1 的 `PrepareProposal` 创建系统交易 3. 系统交易在区块 N+1 期间执行,发出事件 这意味着解绑完成和发出 EVM 事件之间存在一个区块的延迟(大约 0.7 秒)。对于大多数用例,此延迟是可以接受的,因为解绑期本身为 7 天。 #### 高负载场景 如果解绑完成的到达速度快于每个区块 100 个,它们会在队列中累积。队列按 FIFO 顺序处理,因此最旧的完成始终首先通知。 在持续的高负载期间,队列可能会暂时增长。但是,一旦高峰消退,完成较少的后续区块将逐渐排空队列。该系统旨在处理突发而不丢失事件。 ### 未来扩展 系统交易机制为将任何 Stable SDK 操作桥接到 EVM 事件空间提供了通用模式。虽然目前仅用于解绑完成,但该架构可以扩展以涵盖其他用例: #### 质押操作 除了解绑之外,其他质押事件可以发出 EVM 通知: * 验证者的佣金率变化 * 验证者入狱和出狱 #### 治理执行 当治理提案通过并执行时,系统交易可以发出带有提案 ID 和执行结果的事件。这将允许 dApps 对参数变化或升级做出反应,而无需轮询治理模块。 #### 通用事件桥 该模式可以推广为可配置的事件桥,其中每个模块注册哪些 SDK 事件应该镜像到 EVM。这将提供对所有 Stable SDK 操作的全面可见性,而无需每个模块的自定义逻辑。关键架构原则是系统交易仍然是协议级功能,仅由验证者在区块提案期间创建。 ## 技术概览 从状态数据库、执行引擎、共识机制,到针对 USDT 的特定优化,Stable 的设计始终聚焦于性能、可扩展性与可靠性。技术栈中的每一层组件都经过专门优化,以支持高吞吐的工作负载和无缝的 USDT 原生操作。 Tech Overview ### StableBFT Stable 区块链最初采用 **StableBFT** —— 基于 CometBFT 构建的定制化 PoS 共识协议,确保网络具备高吞吐、低延迟和强健的可靠性。为了进一步优化共识性能,Stable 计划拆解数据传播与共识流程,并实现面向区块提议者的交易直接广播机制。 Stable 还计划将协议升级为基于有向无环图(DAG)的 **Autobahn** 架构。在 Autobahn 上构建的 StableBFT 将带来: * 消除单领导瓶颈,实现提议过程的并行化; * 通过将数据传播与排序拆解,加快最终性的达成速度; * 借助增强的 BFT 机制提升对网络异常的鲁棒性。 ### Stable EVM **Stable EVM** 是 Stable 的以太坊智能合约的兼容执行层,支持用户通过现有的以太坊工具和钱包(如 MetaMask)与链进行交互。为打通 Stable EVM 与 StableSDK 的能力边界,同时 Stable EVM 引入一系列 预编译合约,允许 EVM 智能合约安全且原子性地调用Stable底层接口。 Stable 计划通过引入 **StableVM++** 来进一步提升 EVM 执行性能,该模块集成了如 EVMONE 等替代 EVM 实现,以及基于 Block-STM 的 **Optimistic并行执行引擎(OPE)**。 ### StableDB **状态:v1.4.0 升级时上线** Stable 通过解决区块执行后低效的磁盘存储这一主要瓶颈,显著提升了处理速度。它将状态提交与存储操作进行拆解,使得新区块能在无需等待数据存储的情况下迅速处理。同时,结合 `MemDB` 与 `VersionDB`,并使用 `mmap`(内存映射)技术,实现近期数据驻留内存、历史数据高效存储,大幅提升整体吞吐量。 ### 高性能 RPC 即使底层区块链再快,如果 RPC 反应速度缓慢,用户体验仍会大打折扣。Stable 通过彻底重构传统 RPC 架构来解决这一问题。传统的单一 RPC 模式存在资源竞争严重、扩展性差等问题,而 Stable 引入了 **路径分离架构(split-path architecture)**,根据不同功能将操作分离,部署轻量化、专业化的 RPC 节点以实现更快速响应。 未来计划包括为 EVM `view` 调用优化的专用 RPC 节点,以及原生集成索引器(indexer)以进一步加速 dApp 数据访问。 ## 技术路线图 ### Stable 针对稳定币的全栈优化路径 从用户提交交易到最终结果确认,交易生命周期要经历多个阶段:交易首先通过 RPC 广播,然后进入内存池(mempool),然后被打包进区块,经过共识验证、执行,最终将结果状态写入数据库。只有完成这些步骤,用户才能获得最终的交易结果。 这个过程中任何一个环节存在瓶颈,都会影响整体系统性能。Stable 致力于优化交易路径中的每一步,以最大化提升性能并减少延迟。 Stable 的核心技术将分阶段推出,每一阶段都旨在提升每秒交易数(TPS),同时不牺牲交易的终局性。计划中的优化包括状态数据库处理、执行层、共识机制以及 USDT 特定流程的优化。 下文将概述当前区块链架构中的常见性能瓶颈,以及 Stable 的优化方案。 技术路线图 ### 第 1 阶段 #### StableBFT Stable 区块链最初将采用 StableBFT,这是一种基于 CometBFT 的定制化 PoS 协议,提供高吞吐、低延迟与强可靠性。该协议具备终局性,能容忍三分之一的节点故障。未来,Stable 将升级为基于 DAG 的共识机制,实现 5 倍的共识速度提升。 #### USDT 作为原生 Gas 在 Stable 上,USDT0 作为其原生 Gas 代币。USDT0 同时作为用于支付 Gas 和进行价值转移的原生资产,并作为支持 `approve`、`transfer`、`transferFrom` 和 `permit` 的 ERC20 代币。 #### Stable Pay与 Stable Name Stable Pay使用直观设计和社交登录等功能,而已有用户则可直接连接现有钱包,无需迁移。Stable 钱包将提供网页版钱包和原生的手机钱包,保障用户在任何设备上都能安全访问数字资产。 与钱包配套的是 Stable Name:这是一套友好的别名系统,用于替代繁琐且容易出错的 EVM 地址格式,采用唯一且可读性强的标识符。用户只需通过 Stable Name 即可完成收发代币,免去管理十六进制字符串的烦恼。该系统大大减少了交易出错的几率,提升了与加密资产交互的整体体验,使 Stable 成为进入区块链生态的便捷入口。 ### 第 2 阶段 #### **并行执行** 60–80% 的交易之间并不冲突,可以安全地并行执行。但大多数区块链系统仍采用顺序执行,造成不必要的延迟。 Stable 将采用Optimistic并行执行模型以提高吞吐量。交易在假设无冲突的前提下并行执行,若检测到冲突,则将相关交易回滚并按顺序重新执行。此模型在确保正确性的同时带来显著的性能提升。 #### **状态数据库优化** 区块链性能的主要瓶颈之一是磁盘 I/O 慢。在执行完区块后,状态需被提交(commit)和存储。传统模式中,验证节点需等待状态完全存储完毕后才能继续执行下一区块。 Stable 通过拆解状态提交与状态存储来优化该流程。验证节点可在内存中提交最新状态,从而继续下一轮区块执行,同时允许历史状态延迟写入磁盘。这显著降低了执行延迟。 此外,Stable 计划采用 `mmap`(内存映射文件)机制,将文件放置到内存中,加速存储性能。在内存中实时提交状态,同时将归档状态写入磁盘,大幅减少磁盘 I/O 延迟,并提升读写吞吐量。 #### **USDT 转账聚合器** 为支持大规模 USDT0 转账,Stable 将实现转账聚合机制。USDT0 的转账交易将被批量打包处理,从而降低每笔交易的系统开销,提高整体吞吐量。 #### **企业专用区块空间** 企业在使用区块链基础设施时,通常需要可预测的交易延迟,而在网络拥堵时,这种可预测性可能下降。 Stable 通过专用区块空间模型解决该问题,为企业客户保证固定比例的区块容量。该保障机制包括: * 验证节点级定制:验证节点为企业客户预留空间。 * 专属 RPC 节点:企业交易通过独立的内存池和 API 接口优先处理。 该模式确保关键企业在不同网络条件下也能保持稳定性能。 ### 第 3 阶段 #### **基于 Autobahn 的高级共识(StableBFT)** 第一代基于 DAG 的 BFT 引擎(如 Narwhal 和 Tusk)表明,通过将数据传播与共识排序解耦拆解,可消除单个 proposer 的瓶颈。然而,直接将这类系统引入 CometBFT 环境会与高度依赖区块高度的开发习惯及传统内存池设计发生冲突。 Autobahn 提供了一种 PBFT-on-DAG 架构,更自然地与 Stable 的共识层集成。基于 Autobahn 构建的 StableBFT 将实现: * 消除单领导者限制,支持并行交易处理; * 通过分离数据传播与最终排序,加快交易确认速度; * 通过强健的 BFT 机制提升对网络攻击的抵抗性。 根据内部测试结果,该共识协议已在可控环境下实现超过 200,000 TPS(仅共识层)的处理能力。 #### **StableVM++** StableVM++ 是高性能执行引擎,将用 C++ 实现替代当前基于 Go 的 EVM。此更替预计将带来最高 6 倍的执行速度提升,极大增强链上处理能力。 #### **高性能 RPC** 高吞吐量的去中心化应用依赖于快速准确的 RPC 与索引服务。Stable 的高性能 RPC 架构将包括: * **节点级优化**:实时链状态处理,提升 RPC 响应速度; * **集成式索引器**:低延迟索引服务,在保证一致性的情况下提升应用层 API 效率 ; * **强健的 Pub/Sub 系统**:可扩展的 WebSocket 架构,提供可靠的推送和事件通知机制; * **混合负载均衡器**:基于操作类型智能分配流量,最大化利用资源并减少瓶颈。 这些优化将使 Stable 能为 dApp 和企业用户提供稳定且可扩展的访问接口。 ## 即将推出的应用场景 Stable 正在构建超越简单转账和 API 计费的支付模式。下面的案例涵盖了有时间保障的结算、保护隐私的支付以及自主代理商务。其中一些功能目前已有早期形式可用;另一些则依赖于 Stable 目前正在开发中的功能。 ### 有保障的结算 由预留区块容量支持的可靠支付结算,确保无论网络状况如何,交易都能被包含。 #### 概念 有些支付是定时结算周期的一部分,而非独立的转账。在这些流程中,结算必须在周期关闭之前完成,以便下一个状态转换能够按计划进行。如果所需的支付被延迟超过该窗口,周期可能会失败、顺延到下一个窗口,或需要手动恢复。 Stable 的 [有保障的区块空间](/cn/explanation/guaranteed-blockspace) 通过为符合条件的支付流程预留执行容量来解决这一问题。其目标不仅仅是更快的结算,而是在精确的时间约束内实现运营上可靠的完成。 #### 预期场景 一个代币化资产平台每隔几分钟运行一次定时的 DvP(货银对付,Delivery versus Payment)结算。现金部分以批次形式提交,只有当整个支付批次在当前结算周期关闭之前被包含时,证券才会被释放。在正常情况下,这会立即完成。在突发流量期间,部分包含会导致结算周期失败或顺延。借助有保障的结算,平台为支付批次预留容量,使周期能够确定性地关闭。 #### 实现条件 有保障的区块空间将链上支付转变为可调度的操作。结算周期可以基于严格的时间假设来设计,批量支付可以在单个窗口内原子化地提交,上游系统可以将区块包含视为一种依赖关系,而非一种期望。 ### 隐私支付 保护隐私的 USDT0 转账,所选交易细节对公开观察者屏蔽,同时对交易各方和授权审计员保持可验证。 #### 概念 标准的链上转账完全透明;任何人都可以看到发送方、接收方和金额。对于商业支付而言,这种透明度可能会将商业敏感信息暴露给任何监控链的人。 Stable 正在开发 [隐私转账](/cn/explanation/confidential-transfer),这是一个使用零知识密码学的隐私层,可为链上交易实现选择性的保密。被屏蔽的数值只有交易相关方和授权的监管审计员才能访问。 #### 预期场景 一家大型零售商在链上与多个供应商结算库存采购。在透明的链上,竞争对手可以监控这些交易,以反向推断供应商关系、订单量和批发价格。借助隐私转账,商业敏感的细节被屏蔽,而链上记录仍可作为可验证的结算凭证,供双方和授权审计员使用。 ### 代理之间的支付 由 AI 代理自主发起和结算的支付,在交易环节中无需人工批准或干预。 #### 概念 随着 AI 代理承担越来越多的运营任务,它们将需要从其他代理那里采购服务。在当前的工作流程中,这需要人工参与来批准每笔采购、选择供应商或验证交易对手是否可信。代理之间的支付通过让代理在单个交易环节中自主查找、评估和支付服务,消除了这一瓶颈。 这种模式依赖于若干新兴协议的协同工作:代理发现与信任([ERC-8004](https://eips.ethereum.org/EIPS/eip-8004))、安全通信([XMTP](https://xmtp.org))以及能够实时结算的支付通道([x402](/cn/explanation/x402))。 #### 预期流程 1. **发现**:买方代理查询 ERC-8004 身份注册表,以查找提供所需能力(例如图像生成)的代理。注册表返回匹配的代理身份及相关元数据。 2. **验证**:买方在 ERC-8004 注册表中检查每个候选者。身份、信誉评分和验证证明决定了哪些提供方足够可信,可以进行交易。 3. **协商**:买方通过 XMTP 向选定的提供方发送任务参数。两个代理通过加密消息就价格、截止时间和交付格式达成一致。 4. **支付**:买方调用提供方的 HTTP 端点。提供方返回 402 响应。买方签署 ERC-3009 授权,并附带支付头重试。促成方在 Stable 上结算支付,提供方返回结果。 5. **评价**:交付后,买方向 ERC-8004 信誉注册表提交反馈,更新提供方的评分以供未来交互参考。 #### 实现条件 代理之间的支付将服务采购转变为一个完全可编程的环节。代理可以比较提供方、切换供应商,并实时结算支付,而无需人工调度或审批队列。这使得构建自主供应链成为可能,代理可以以机器速度持续采购、支付和交付服务,将商务扩展到超越手动协调所能支持的范围。 ### 下一步推荐 * [**有保障的区块空间**](/cn/explanation/guaranteed-blockspace) — 了解有保障结算背后的协议级机制。 * [**隐私转账**](/cn/explanation/confidential-transfer) — 查看 Stable 正在构建的隐私模型。 * [**x402**](/cn/explanation/x402) — 理解代理之间流程背后的结算协议。 ## USDT as Gas Stable 是围绕 USDT 稳定币构建的。USDT0 是 USDT 的原生跨链版本,是支撑 Stable 生态系统的核心资产。 ### 摘要 Stable 是一个使用 USDT0 作为原生 Gas 代币的 EVM 兼容区块链。USDT0 同时作为 Gas 支付和价值转移的原生资产,以及支持 `approve`、`transfer`、`transferFrom` 和 `permit` 的 ERC20 代币。 这种设计让交易成本可预测并以美元计价,简化了用户体验。然而,它引入了与以太坊不同的行为差异,影响余额语义、授权安全性和某些操作码假设。 本文档指定了 Stable 的 USDT0 Gas 机制,描述了由此产生的行为差异,并定义了在 Stable 上部署的智能合约所需和推荐的开发模式。 ### 版本说明 随着 Stable v1.2.0,USDT0 成为 Stable 上的原生 Gas 代币,取代了 gUSDT。作为此过渡的一部分: * gUSDT 即将下线。 * 现有的 gUSDT 余额会自动转换为 USDT0。 * 用户和应用程序不再需要封包和解包代币来支付费用或转移价值。 在 v1.2.0 之后,USDT0 同时作为: * 网络费用资产(gas),以及 * 具有 `approve`、`permit`、`transfer` 和 `transferFrom` 的标准 ERC20 代币。 ### 网络地址 USDT0 代币合约地址: * 测试网:[0x78cf24370174180738c5b8e352b6d14c83a6c9a9](https://testnet.stablescan.xyz/token/0x78cf24370174180738c5b8e352b6d14c83a6c9a9) * 主网:[0x779ded0c9e1022225f8e0630b35a9b54be713736](https://stablescan.xyz/token/0x779ded0c9e1022225f8e0630b35a9b54be713736) ### 术语 * **Stable**:一个 EVM 兼容的区块链,其中 USDT0 是原生 Gas 代币。 * **USDT0**:USDT 的原生跨链版本,同时作为: * 用于 Gas 和价值转移的原生资产,以及 * 具有授权和许可语义的 ERC20 代币。 * **原生余额**:由 `address(x).balance` 返回的余额,以 USDT0 计价。 * **Gas 费**:在 EIP-1559 式费用市场下计算的以 USDT0 支付的交易费用。 ### 什么是 USDT0? USDT0 是使用 LayerZero 的全链可替代代币 (OFT) 标准的 USDT 的原生跨链版本。USDT0 与 USDT 1:1 锚定,旨在跨多个区块链移动,而无需传统的桥接工作流程或封包表示。 在跨链转移 USDT0 时,代币在某些源链上被锁定(取决于链的原生 USDT 支持)或销毁,然后通过 LayerZero 的跨链消息在目标链上铸造。这保持了 1:1 锚定,同时将流动性整合到单个可互操作的资产中,而不是分散的链本地池。 对于用户,这可以实现更快的入门、降低的操作复杂性和改进的流动性流动性。 ### USDT0 和 Stable USDT0 是支撑 Stable 链上经济和日常使用的核心资产。由于同一资产用于支付费用和转移价值,Stable 减少了以下方面的摩擦: * **普通用户**:更简单的入门和更少的代币概念 * **开发人员**:更简单的费用和价值流 * **企业**:简化的会计和财务运营 Stable 还可以通过允许用户通过 LayerZero 从其他网络入门 USDT0 来从第一天开始访问深度 USDT 流动性。 ### 假设和先决条件 对于以下内容,读者应该理解: * Solidity 执行语义和原生价值转移 * ERC20 授权机制和许可流程 * 标准智能合约安全模式,包括 Checks-Effects-Interactions ### 1. Gas 和费用模型 #### 1.1 概述 Stable 以 USDT0 计价所有交易费用。Gas 定价遵循 EIP-1559 式模型,具有动态调整的基础费用。 交易费用定义为: ``` fee = gasUsed × baseFee ``` 交易可以使用标准 EIP-1559 参数指定 `maxFeePerGas`。 *注意:Stable 不支持优先小费。不要设置 `maxPriorityFeePerGas`,否则小费金额将丢失。* #### 1.2 交易提交 客户端应从最近的区块获取最新的基础费用,并在计算 `maxFeePerGas` 时包含安全边际。 示例(说明性): ```javascript const block = await provider.getBlock("latest"); const baseFee = block.baseFeePerGas; const maxPriorityFeePerGas = 1n; const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; ``` #### 1.3 获取 USDT0 账户通过以下方式获取 USDT0: * 从其他支持的链桥接 USDT0 * 从 Stable 上的其他账户接收转账 ### 2. Stable 如何启用 USDT0 作为 Gas 代币 Stable 使用预扣费和退款结算模型在 USDT0 中收取 Gas 费。 #### 示例交易 Alice 向 Bob 发送 100 USDT0。 #### 2.1 Ante-handler 阶段 在 `MonoEVMAnteHandler` 中的交易验证期间: 1. 读取 Alice 的 USDT0 余额。 2. 协议验证 Alice 可以覆盖: * 交易价值(100 USDT0),以及 * 最大可能的 Gas 费(`gasWanted × fee`)。 3. 预先转移最大 Gas 费: * `alice → fee_collector` 以 USDT0。 #### 2.2 执行阶段 在 `ApplyTransaction` 期间: 1. EVM 执行交易。 2. 记录实际 Gas 消耗。 3. 应用价值转移: * `alice → bob` 转移 100 USDT0。 #### 2.3 结算阶段 执行后: 1. 协议计算预扣费的未使用部分: ``` refund = (gasWanted − gasUsed) × baseFee ``` 2. 退还未使用的费用: * `fee_collector → alice` 以 USDT0。 ### 3. 余额语义和行为差异 #### 3.1 原生余额可变性 在以太坊上,合约的原生余额通常仅因合约执行而改变。 在 Stable 上,合约的原生 USDT0 余额也可能由于基于 ERC20 授权的操作而改变,包括 `transferFrom` 和 `permit`。这些操作可以在不调用任何合约代码的情况下减少合约的原生余额。 因此,以下假设在 Stable 上无效: * 合约的原生余额只能在合约被调用时减少。 ### 4. 合约设计要求 #### 4.1 禁止模式:镜像余额会计 合约不得依赖内部变量来镜像原生余额。 不安全模式的示例: ```solidity uint256 public deposited; function deposit() external payable { deposited += msg.value; } ``` 如果通过基于授权的转移耗尽 USDT0,此类变量可能与实际原生余额不同。 #### 4.2 必需模式:实际余额偿付能力检查 所有原生价值转移必须在转移之前立即使用 `address(this).balance` 验证偿付能力。 示例: ```solidity require(address(this).balance >= amount, "insufficient balance"); ``` 提款必须遵循 Checks-Effects-Interactions 顺序: ```solidity uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount); payable(msg.sender).call{value: amount}(""); ``` #### 4.3 状态进展必须独立于余额 依赖进展、里程碑或完成条件的协议逻辑必须使用非余额状态变量(如计数器或纪元)显式跟踪这些内容。 原生余额只能在支付时用于偿付能力验证。 #### 4.4 授权暴露 托管用户资金的合约不应向外部地址授予 USDT0 授权。 如果无法避免授权,合约应: * 仅批准确切金额 * 使用后立即重置授权 * 将余额排空风险视为已知限制 ### 5. 地址状态假设 #### 5.1 EXTCODEHASH 合约不得依赖 `EXTCODEHASH(addr) == 0x0` 来推断地址从未被使用过。 任何地址使用的概念都必须在合约状态中显式跟踪。 示例: ```solidity mapping(address => bool) public used; ``` ### 6. 零地址处理 在 Stable 上: * 向 `address(0)` 的原生 USDT0 转移会回滚。 * 向 `address(0)` 的 ERC20 USDT0 转移也会回滚。 没有通过转移到零地址来销毁 USDT0 的支持机制。 合约必须: * 明确拒绝 `address(0)` 作为接收者 * 重新设计任何假设零地址销毁的逻辑 * 如果需要不可逆丢失语义,请使用显式接收合约 ### 7. 测试要求 Stable 部署的测试套件应包括: * 基于授权的排空场景(`approve` + `transferFrom`) * 使用实际原生余额的偿付能力执行 * 不依赖 `EXTCODEHASH` 的地址使用逻辑 * 零地址转移的显式失败情况 ### 8. 迁移检查清单 将合约从以太坊移植到 Stable 时: * 删除内部原生余额镜像 * 用 `address(this).balance` 替换所有偿付能力检查 * 删除所有到 `address(0)` 的原生或 ERC20 转移 * 审计所有 USDT0 批准 * 添加涵盖许可和基于授权流程的测试 ### 9. 总结 Stable 使用 USDT0 作为 Gas 代币提供了可预测的费用和统一的价值会计,同时改变了关于原生余额行为的核心假设。 Stable 上的正确合约设计需要: * 将 USDT0 视为双重角色资产 * 针对实际余额执行偿付能力 * 避免基于授权的余额排空 * 消除对以太坊特有的余额及地址假设的依赖 ### 常见问题 **我们现在使用 USDT0 作为封包的原生代币。升级后,哪个代币应该被视为封包的原生代币?** 升级后,USDT0 既是原生代币又是 ERC-20 代币。您应该直接使用 USDT0,不再需要封包或解包。 **原始的 USDT0 合约地址(`0x779Ded0c9e1022225f8E0630b35a9b54bE713736`)会发生什么变化?** 没有任何变化。相同的地址仍然有效并继续代表 USDT0。 **升级后,原生代币地址是 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`(而不是 `0x0000000000000000000000000000000000001000`)吗?** 是的。升级后,原生代币标识符/地址是 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`。 **那么 `0x0000000000000000000000000000000000001000` 呢?它还会作为 gUSDT 的代币地址使用吗,我们应该保留它吗?** 不会。您可以删除它。升级后将不再使用它。 **对于 DEX calldata,协议是否会停止使用 `0x0000000000000000000000000000000000001000` 作为"原生代币"标识符,而改用 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`?** 正确。升级后,DEX 应使用 `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` 作为原生代币标识符。 ## 概览 ### USDT 专属功能 Stable 是一条专为 USDT 打造的 Layer 1 区块链网络。协议的每一个组件都围绕 USDT 的无摩擦转账进行优化,致力于为全球最广泛使用的稳定币提供一个高性能、简洁流畅的运行环境。 除了核心基础设施外,Stable 还提供一系列 USDT 专属功能,进一步提升整体用户体验: * **USDT 作为 Gas Token**:USDT0 作为其原生 Gas 代币。USDT0 同时作为用于支付 Gas 和进行价值转移的原生资产,并作为支持 `approve`、`transfer`、`transferFrom` 和 `permit` 的 ERC20 代币。 * **保证区块空间(Guaranteed Blockspace)**:保证区块空间是一种专为企业客户设计的区块容量分配模型,可保证其在任何网络条件下都拥有固定份额的区块资源,实现确定性延迟与费用。 * **USDT0 转账聚合器(Transfer Aggregator)**:通过隔离并聚合 USDT0 转账交易,实现极致吞吐量的同时,确保公平性且不影响其他交易类型。 * **隐私转账(Confidential Transfer)**:Stable 计划采用零知识加密(ZK Cryptography)实现加密的 USDT 转账。交易金额将在链上隐藏,同时保留发送方与接收方地址公开,以满足合规审计需求。 ## USDT 转账聚合器 作为一条专为 USDT 交易优化的区块链,Stable 被设计用于处理极高频次的代币转账,同时保持系统的整体响应。为了在优化 USDT 特定性能的同时兼顾一般交易的多样性,Stable 引入了 USDT 转账聚合器机制。这是一种高效、可扩展的解决方案,可对 USDT0 转账进行高度并行化和容错处理。 ### 为什么需要 USDT 转账聚合器? 支持大规模 USDT 使用的挑战在于如何同时优化吞吐量和保持不同交易处理的公平性: * 传统的 ERC20 代币转账按顺序处理,在高交易负载下成为性能瓶颈。 * 如果仅优先考虑 USDT 的吞吐量,可能会排挤其他交易,导致整条链的性能下降。 USDT 转账聚合器通过将 USDT 转账隔离优化,避免影响执行流程中的其他部分,从而解决了这一矛盾。 ### 并行聚合与验证 > *以下内容基于当前战略规划,属于前瞻性设计。随着经验积累和优先级调整,路线图可能会有所更新。* 聚合系统的核心是一个可并行化的聚合与验证管道,灵感来自 `MapReduce` 计算模型。系统不再按顺序处理每一笔转账,而是以批次为单位,聚合所有账户的输入输出后,再统一进行余额更新。 #### 关键流程 1. **账户差异聚合(Aggregate Account Diffs)** * 每笔转账都映射到发送方与接收方。 * 为每个账户生成一个差异日志(diff journal),代表该账户的净变化: * 发送总额为负值。 * 接收总额为正值。 2. **余额验证** * 系统确保全局余额守恒:总输入等于总输出。 * 每个账户的净变动通过并行方式独立验证资金是否充足。 * 资金不足的账户将被标记,但不会影响该批次执行。 3. **MapReduce 并行模型** * **Map 阶段**:根据所有收支转账计算每个账户的净变化值。 * **Reduce 阶段**:聚合这些变化以确定最终状态更新。 ### 技术亮点 #### 并行计算模型 * 利用预编译合约实现余额校验与差异计算的并行处理。 * 相比传统的串行 ERC20 执行,大幅减少执行时间。 #### 依赖关系分析 * 识别转账之间的依赖关系(如多个发送来自同一账户)。 * 预先标记高风险转账(如可能余额不足),以减少级联失败。 #### 模块化故障处理 * 转账在账户级别上被隔离,只有出现问题的账户会被影响。 * 无冲突的转账将正常执行与确认,不受影响。 #### 选择性失败处理 传统的转账处理在一个区块中通常是“要么全部成功,要么全部失败”。Stable 的聚合模型引入了更精细的账户级失败隔离机制: * 若某账户的 `当前余额 + 净变化 < 0`,系统只标记该账户的转账失败。 * 涉及其他账户的转账将正常继续执行。 * 此选择性回滚机制确保无效或恶意转账不会破坏整个批次的完整性。 ### 提议者驱动或声誉排序机制 为进一步优化执行并避免状态冲突,Stable 对聚合转账引入预处理排序机制: * **基于声誉的排序**:拥有良好交易历史或信誉的账户优先执行,降低失败率与重新排序成本。 * **提议者排序**:可信提议者节点可根据依赖关系排序交易,减少冲突并提高吞吐。 * **聚合转账优先执行**:聚合后的 USDT 转账优先于一般交易执行,减少依赖冲突,释放执行空间。 Stable 的 USDT 转账聚合器机制是一项针对性优化,能够在不影响通用交易处理性能的前提下,最大化 USDT0 转账的吞吐量。通过结合并行执行、模块化失败处理与智能排序策略,Stable 为以稳定币驱动的经济体提供了高扩展性的基础架构,使快速、频繁、无摩擦的代币转账成为常态。 ## USDT0 在 Stable 上的行为 **如果你正在从以太坊移植合约,请在部署前阅读本页。** Stable 上的 USDT0 既是原生 gas 代币,又是基于同一余额的 ERC-20 代币。因此,四种以太坊上的假定行为会失效:合约的原生余额可能在没有调用该合约的情况下发生变化;`EXTCODEHASH` 可能在零值和空哈希之间来回切换;向零地址转账会回滚;以及由于分数余额对账,一笔逻辑上的转账可能发出多个 `Transfer` 事件。 本页将逐一讲解每种情况并给出安全的合约模式。如果你只读一节,请阅读[迁移清单](#migration-checklist)。它是"把你的以太坊合约移植到这里"的摘要。 ### 双重角色概述 Stable 上的 USDT0 既是原生 gas 代币,又是 ERC-20 代币。这种双重角色模型会影响余额行为、合约设计和事件处理。下面各节将逐一讲解双重角色改变预期行为的每种情况。 有关 USDT0 为何以这种方式运行的背景信息,请参阅 [USDT 作为 gas](/cn/explanation/usdt-as-gas-token)。要通过真实转账体验这种行为,请参阅[发送你的第一笔 USDT0](/cn/tutorial/send-usdt0)。 ### 余额对账 USDT0 作为原生资产使用 18 位小数,作为 ERC-20 代币使用 6 位小数。原生转账和 ERC-20 转账操作于同一底层余额,但 12 位的精度差距意味着当转账涉及亚整数精度时,系统必须对账分数金额。 ``` before 0.000001 USDT0 (ERC-20) + 0.000000000000000000 USDT0 (internal) // address(account).balance = 0.000001000000000000 // USDT0.balanceOf(account) = 0.000001 if transfer 0.0000001 USDT0 to another account after 0.000000 USDT0 (ERC-20) + 0.000000900000000000 USDT0 (internal) // address(account).balance = 0.000000900000000000 // USDT0.balanceOf(account) = 0.000000 ``` 这可能导致 `address(account).balance` 和 `USDT0.balanceOf(account)` 之间相差最多 0.000001 USDT0。 ### 事件处理 每次对账转账都会发出一个额外的 `Transfer` 事件。根据发送方和接收方的分数余额受到何种影响,一笔逻辑上的 USDT0 转账最多可产生两个额外的 `Transfer` 事件: * **发送方调整**:如果发送方的分数余额不足,0.000001 USDT0 会从发送方转移到储备地址。这会发出一个额外的 `Transfer` 事件。 * **接收方调整**:如果接收方的分数余额溢出,0.000001 USDT0 会从储备地址转移到接收方。这会发出一个额外的 `Transfer` 事件。 * **两者均调整**:如果两个条件在同一笔转账中同时发生,则绕过储备地址。发送方作为主转账的一部分直接向接收方转移 0.000001 USDT0。不会发出额外事件。 这些辅助事件涉及储备地址 `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`。通过重放 `Transfer` 事件来跟踪 USDT0 余额的索引器和链下服务,必须过滤或考虑与此地址之间的转账。 ### 合约设计要求 #### 原生余额可变性 在以太坊上,合约的原生余额通常只会因合约执行而发生变化。在 Stable 上,合约的原生 USDT0 余额还可能因基于 ERC-20 授权的操作(包括 `transferFrom` 和 `permit`)而发生变化。这些操作可以在不调用任何合约代码的情况下减少合约的原生余额。 因此,以下假设在 Stable 上无效: > 合约的原生余额只有在合约被调用时才会减少。 #### 不要镜像原生余额 在以太坊上,使用内部变量跟踪存款很常见。在 Stable 上,这是不安全的,因为 ERC-20 `transferFrom` 可以在外部耗尽原生余额。 ```solidity // UNSAFE on Stable uint256 public deposited; function deposit() external payable { deposited += msg.value; } ``` #### 转账前始终检查真实余额 所有原生价值转账都必须在转账前使用 `address(this).balance` 验证偿付能力,而不是使用内部记账变量: ```solidity // SAFE function withdraw() external { uint256 amount = credit[msg.sender]; credit[msg.sender] = 0; require(address(this).balance >= amount, "insufficient balance"); payable(msg.sender).call{value: amount}(""); } ``` #### 状态推进必须与余额无关 依赖于推进、里程碑或完成条件的协议逻辑,必须使用非余额状态变量(例如计数器或纪元)显式跟踪这些条件。原生余额应仅在支付时用于偿付能力验证。 #### 禁止零地址转账 在 Stable 上,向 `address(0)` 的原生转账和 ERC-20 转账都会回滚。 ```solidity // REVERT on Stable payable(address(0)).call{value: amount}("") USDT0.transfer(address(0), amount); ``` 任何发送原生 USDT0 的合约逻辑都应在转账调用前验证接收方并明确拒绝 `address(0)`: ```solidity // SAFE require(recipient != address(0), "zero address recipient"); payable(recipient).call{value: amount}(""); ``` 如果合约使用零地址转账作为销毁机制,则必须重新设计。如果需要不可逆的损失语义,请使用显式的接收(sink)合约。 #### EXTCODEHASH 行为 在以太坊上,`EXTCODEHASH` 操作码返回: * **零哈希**(`0x0000...`):如果某地址从未被使用过(nonce=0、balance=0、无代码)。 * **空哈希**(`0xc5d2…a470`,即空代码的 Keccak-256 哈希):如果某地址存在但没有代码。 在以太坊上,一旦地址从零哈希转变为空哈希,就无法再回到零哈希。在 Stable 上,由于 USDT0 支持基于 `permit()` 的授权,地址可以在不发送交易的情况下创建授权。结合 `transferFrom()`,这允许在 nonce 不增加的情况下改变原生余额,从而可能使 `EXTCODEHASH` 在零哈希和空哈希之间来回切换。 ```solidity // UNSAFE on Stable function isUnusedAddress(address addr) public view returns (bool) { bytes32 codeHash; assembly { codeHash := extcodehash(addr) } return codeHash == bytes32(0); } ``` 请改用显式跟踪: ```solidity // SAFE contract SafeAddressTracker { mapping(address => bool) public hasBeenUsed; function markAsUsed(address addr) internal { hasBeenUsed[addr] = true; } function isUnused(address addr) public view returns (bool) { return !hasBeenUsed[addr]; } } ``` ### 测试要求 Stable 部署的测试套件应包括: * 基于授权的耗尽场景(`approve` + `transferFrom`) * 使用真实原生余额强制执行偿付能力 * 不依赖 `EXTCODEHASH` 的地址使用逻辑 * 零地址转账的显式失败用例 ### 迁移清单 从以太坊向 Stable 移植合约时: * 移除内部原生余额镜像 * 将所有偿付能力检查替换为 `address(this).balance` * 移除所有向 `address(0)` 的原生或 ERC-20 转账 * 审计所有 USDT0 授权 * 添加涵盖 `permit` 和基于授权流程的测试 * 验证链下索引器能处理来自分数余额对账的辅助 `Transfer` 事件 ### 关键要点 在 Stable 上正确的合约设计需要: * 将 USDT0 视为双重角色资产 * 针对真实余额强制执行偿付能力 * 避免基于授权的耗尽路径 * 消除对以太坊特定余额和地址假设的依赖 链下服务和索引器应: * 考虑来自分数余额对账的辅助 `Transfer` 事件 * 使用直接的余额查询,而不是基于事件的余额重建 ### 推荐的下一步 * [**USDT 作为 gas**](/cn/explanation/usdt-as-gas-token) — 了解 USDT0 为何同时作为原生资产和 ERC-20 代币运行。 * [**发送你的第一笔 USDT0**](/cn/tutorial/send-usdt0) — 在测试网上通过原生和 ERC-20 路径提交 USDT0 转账。 * [**以太坊对比**](/cn/explanation/ethereum-comparison) — 查看从以太坊移植时的每一项行为差异。 ## 跨链桥接到 Stable USDT 通过两条桥接路径之一进入 Stable,具体取决于它在源链上的形态。这两条路径都会将 USDT0 交付到用户在 Stable 上的钱包。 :::note **两条路径,同一结果:** * **OFT Mesh**:源链已经拥有 USDT0。在源链上销毁,在 Stable 上铸造。跨链 1:1。示例:Arbitrum、Ethereum、Optimism、Polygon、Unichain、Ink、Bera、Mantle、Hyperliquid、MegaETH(共 21 条链)。 * **Legacy Mesh**:源链仅有原生 USDT。通过 Arbitrum 作为枢纽进行路由。对转账金额收取 0.03% 费用。示例:Tron、TON。 ::: 下面各节将详细描述每条路径。 ### USDT0 OFT Mesh 与 Legacy Mesh Stable 参与两个互补的跨链转账网络。 #### OFT Mesh 任何支持 USDT0 的链都可以参与 OFT Mesh。在 OFT Mesh 内,USDT0 跨链转账保持 1:1 的价值比率。当发生转账时,源链上的 USDT0 代币被销毁,并在目标链上铸造等量的代币。当前 OFT Mesh 参与者包括 Arbitrum、Bera、Conflux、Ethereum、Flare、Hedera、Hyperliquid、Ink、Mantle、MegaETH、Monad、Morph、MP1、Optimism、Plasma、Polygon、Rootstock、Sei、Stable、Tempo、Unichain 和 X Layer。 #### Legacy Mesh 任何拥有原生 USDT(而非 USDT0)的链都可以通过 Legacy Mesh 进行路由。Legacy Mesh 采用枢纽辐射式架构,由 Arbitrum 作为 USDT0 的中央枢纽。该模型利用 Arbitrum 上的 USDT0 流动性池。USDT0 团队对转账金额收取 0.03% 的费用。当前 Legacy Mesh 参与者包括 Tron 和 TON。 Ethereum 和 Arbitrum 同时参与两个 Mesh:这些链上的用户可以通过 OFT 路径(销毁/铸造 USDT0)或 Legacy 路径(通过 Arbitrum 枢纽锁定原生 USDT)进行桥接。 *** ### 路径 1:将 USDT0 桥接到 Stable(OFT 支持的链) 当用户已在 OFT 支持的源链(如 Arbitrum 或 Ink)上持有 USDT0 时,适用此路径。 #### 参与方 | 名称 | 链上? | 负责方 | | --------------------- | --- | --------------- | | 用户 | N/A | 用户 | | USDT0 OUpgradable | ✅ | USDT0 的智能合约 | | LayerZero Endpoint V2 | ✅ | LayerZero 的智能合约 | | MessageLib Registry | ✅ | LayerZero 的智能合约 | | Executor | ❌ | LayerZero Labs | | USDT0 DVN | ❌ | USDT0 | | Canary DVN | ❌ | Canary | | LayerZero DVN | ❌ | LayerZero Labs | #### 流程图 将 USDT0 桥接到 Stable:OFT Mesh 流程 #### 详细步骤 ##### 1. 发起转账(链上,源链) 用户在源链上的 **USDT0 OUpgradable** 合约上调用 `lzSend` 方法。该交易包含消息负载、目标 LayerZero 端点和合约地址,以及诸如 gas 上限和费用等配置参数。 ##### 2. 数据包创建(链上,源链) 源 LayerZero 端点打包 OApp 的消息,使用指定的源 MessageLib 合约对其进行编码,并将其发送到安全栈(DVN)和 Executor,完成发送交易。 ##### 3. 消息验证(链下,DVN) 去中心化验证网络(DVN)在目标合约执行消息之前独立验证消息。只有获得 OApp 授权的 DVN 才能执行验证。USDT0 桥接要求三个 DVN 对每条消息进行签名:LayerZero Labs、Canary 和 USDT0。要查看任何路径上的规范配置,请参阅 [LayerZeroScan 上 USDT0 的 OApp](https://layerzeroscan.com/)。 ##### 4. 标记为可验证(链上,Stable) 一旦所有所需的 DVN 验证了消息,目标 MessageLib 合约就会将其标记为可验证。 ##### 5. 验证提交(链下,Executor) Executor 将经过验证的消息提交到目标 LayerZero 端点,为执行做好准备。 ##### 6. 数据包验证(链上,Stable) 目标 LayerZero 端点确认 Executor 交付的数据包与 DVN 验证的数据包匹配。 ##### 7. 消息执行(链下,Executor) Executor 在目标链上调用 `lzReceive`,触发 Stable 上的 USDT0 OUpgradable 合约进行消息处理。 ##### 8. 完成(链上,Stable) Stable 上的 USDT0 OUpgradable 合约处理经过验证的消息,完成跨链转账。USDT0 被铸造到用户的地址。 *** ### 路径 2:将原生 USDT 桥接到 Stable(Legacy Mesh) 当用户在 Legacy Mesh 链(如 Tron)上持有原生 USDT 时,适用此路径。转账在到达 Stable 之前会通过 Arbitrum 作为中间枢纽进行路由。 #### 参与方 | 名称 | 链上? | 负责方 | | -------------------------- | --- | --------------- | | 用户 | N/A | 用户 | | USDT Pool | ✅ | USDT0 的智能合约 | | USDT0 Pool | ✅ | USDT0 的智能合约 | | MultiHopComposer | ✅ | LayerZero 的智能合约 | | USDT0 OUpgradable | ✅ | USDT0 的智能合约 | | LayerZero Endpoint | ✅ | LayerZero 的智能合约 | | MessageLib Registry | ✅ | LayerZero 的智能合约 | | USDT0 Legacy Mesh Operator | ❌ | USDT0 | | Executor | ❌ | LayerZero Labs | | USDT0 DVN | ❌ | USDT0 | | Canary DVN | ❌ | Canary | | LayerZero DVN | ❌ | LayerZero Labs | #### 流程图 将原生 USDT 从 Tron 桥接到 Stable:Legacy Mesh 流程 #### 详细步骤 ##### 1. 发起转账(链上,Tron) 用户发起桥接交易,并将原生 USDT 发送到 Tron 上的 **USDT Pool** 合约。USDT 被锁定在池中。然后 USDT Pool 合约向 Tron 上的 LayerZero Endpoint 合约发送一条消息。 ##### 2. 向 Legacy Mesh 发送消息(链下) LayerZero Endpoint 合约将消息发送到 **USDT0 Legacy Mesh Operator**,由其验证消息。 ##### 3. 发起 MultiHop 转账(链上,Arbitrum) USDT0 Legacy Mesh Operator 在 Arbitrum 上的 LayerZero **MultiHopComposer** 合约上调用 `lzCompose()` 方法。无需额外的用户交互,MultiHopComposer 合约即可执行从 Arbitrum 到 Stable 的 USDT0 铸造-销毁桥接转账。 :::note MultiHopComposer 合约完全无需许可,并且没有 `owner()`,以确保不可变性。 ::: ##### 4. 将 USDT0 转移到 Stable(链上和链下) 其余步骤遵循与[将 USDT0 桥接到 Stable](#path-1--bridging-usdt0-to-stable-oft-supported-chains)(上述第 1–8 步)完全相同的路径。Arbitrum 上的 USDT0 OUpgradable 合约通过 LayerZero 发送,DVN 进行验证,USDT0 在 Stable 上铸造。 #### 注意事项 * Arbitrum 上的 USDT0 流动性由 USDT0 团队管理。 * Legacy Mesh 对转账金额收取 0.03% 的费用。 * 用户无需直接与 Arbitrum 交互;MultiHop 流程是自动的。 ### 接下来推荐 * [**资金流转**](/cn/explanation/flow-of-funds) — 查看 USDT 从入金到结算的端到端生命周期。 * [**桥接教程**](/cn/tutorial/bridge-usdt0) — 使用 LayerZero OFT Adapter 将测试 USDT 从 Sepolia 桥接到 Stable 测试网。 * [**USDT 作为 gas**](/cn/explanation/usdt-as-gas-token) — 了解该资产到达 Stable 后的作用。 ## 支付与转账 围绕一种既负责转移资金又支付交易费用的资产构建的 P2P 支付和商户结算。 ### 问题所在 在通用型链上,用户必须持有一种单独的 gas 代币(ETH、SOL)才能转移稳定币。这打破了"发送一美元,接收一美元"的心理模型,并在新用户引导环节造成转化流失——因为只持有 USDT 的付款人甚至无法提交转账。 ### Stable 的解决方案 * **USDT0 既是 gas 代币也是支付资产。** 用户只需一种资产即可发送或接收。参见 [USDT 作为 gas](/cn/explanation/usdt-as-gas-token)。 * **Gas 豁免让应用程序可以代用户支付 gas**,从而实现零费用的用户体验,用户无需接触第二种代币。参见 [Gas 豁免](/cn/explanation/gas-waiver)。 * **单槽终结性意味着结算即时完成。** 转账一旦进入区块即为最终确认。参见 [以太坊对比](/cn/explanation/ethereum-comparison)。 ### 后续推荐 * [**USDT 作为 gas**](/cn/explanation/usdt-as-gas-token) — 了解这种同时取代 ETH 用于 gas 和支付的资产。 * [**Gas 豁免**](/cn/explanation/gas-waiver) — 了解应用程序如何通过治理批准的豁免地址来代付用户 gas。 * [**以太坊对比**](/cn/explanation/ethereum-comparison) — 回顾从以太坊迁移时会发生哪些变化(终结性、gas 代币、优先级小费)。 ## 薪资发放与批量支付 为员工、承包商和供应商进行大规模支付,具备可预测的吞吐量、可预测的成本,并为敏感金额提供隐私保护。 ### 问题所在 大批量稳定币支付在共享链上会触及单笔交易吞吐量限制。成本会随网络拥堵而波动,因此昨天还能低成本完成的薪资发放,今天可能就会成本飙升。除此之外,薪资和供应商金额对任何观察链上情况的人都是公开可见的,这会泄露具有商业敏感性的数据。 ### Stable 如何解决 * **USDT 转账聚合器将大批量转账批处理为并行化的结算捆绑包**,因此单次发放不会受到单笔交易开销的瓶颈限制。参见 [USDT 转账聚合器](/cn/explanation/usdt-transfer-aggregator)。 * **保障区块空间为已注册的合作伙伴在每个区块中提供预留容量**,因此无论网络在做什么,纳入和成本都保持可预测。参见 [保障区块空间](/cn/explanation/guaranteed-blockspace)。 * **机密转账使用零知识密码学屏蔽金额**,因此薪资和供应商发放不会在链上公布敏感数字。参见 [机密转账](/cn/explanation/confidential-transfer)。 ### 后续推荐 * [**USDT 转账聚合器**](/cn/explanation/usdt-transfer-aggregator) — 了解大批量 USDT0 转账如何批处理为并行化的结算捆绑包。 * [**保障区块空间**](/cn/explanation/guaranteed-blockspace) — 了解已注册的合作伙伴如何在每个区块中获得预留容量。 * [**机密转账**](/cn/explanation/confidential-transfer) — 了解 ZK 密码学如何屏蔽转账金额,同时保持各方可审计。 ## 私密转账 资金管理操作、供应商付款和工资发放等场景中,金额具有商业敏感性,不应向全世界公开。 ### 问题所在 所有标准的 EVM 转账都是公开可见的。一次工资发放或供应商结算会在链上泄露关键的业务数据:谁向谁付款、付了多少、付款频率如何。竞争对手、交易方以及任何抓取网络数据的人,无需询问就能重建出工资区间、供应商定价和资金动向。 ### Stable 的应对方式 * **保密转账使用零知识密码学来屏蔽转账金额**,同时为合规保持各方可审计,因此敏感数字在不牺牲审计记录的情况下保持私密。参见[保密转账](/cn/explanation/confidential-transfer)。 * **资金流向展示了保密转账在完整 USDT 生命周期中的位置**,从入金到出金。参见[资金流向](/cn/explanation/flow-of-funds)。 ### 下一步推荐 * [**保密转账**](/cn/explanation/confidential-transfer) — 了解 ZK 密码学如何在保持各方可审计的同时屏蔽转账金额。 * [**资金流向**](/cn/explanation/flow-of-funds) — 追踪 USDT 从入金、链上转账到出金结算的全过程。 ## 赞助和无 gas 体验 有些应用希望从用户体验中彻底移除 gas,让首次使用的用户无需先获取第二种资产即可登录并交易。 ### 问题所在 要求用户在使用应用前先获取 gas 代币,会造成一个上手门槛,导致面向消费者的产品转化率骤降。一个只持有 USDT(或者什么都没有)的新用户无法提交交易,而把他们引导到单独的交易所去购买 gas,正是大多数人流失的地方。 ### Stable 如何解决 * **Gas 豁免:经治理批准的豁免地址代表用户提交以零 gas 价格执行的封装交易**,使应用全程承担 gas,用户看到的则是一次免费操作。参见 [Gas 豁免](/cn/explanation/gas-waiver)。 * **EIP-7702 会话密钥让 dApp 持有受限范围、有时限的权限**,从而能够代表用户提交交易,而无需用户对每一笔交易签名。参见 [EIP-7702](/cn/explanation/eip-7702)。 ### 下一步推荐 * [**Gas 豁免**](/cn/explanation/gas-waiver) — 了解经治理批准的豁免如何以零 gas 价格提交封装交易。 * [**EIP-7702**](/cn/explanation/eip-7702) — 理解 EOA 如何将受限范围、有时限的权限委托给 dApp。 ## 将 HTTP 端点货币化 x402 是一个基于 HTTP 构建的支付协议。服务器返回 `402 Payment Required` 以及付款详情,客户端签署一份 [ERC-3009](/cn/explanation/erc-3009) 授权,促成方在链上结算。整个交换过程通过标准的 HTTP 标头完成。客户端只需要一个钱包:无需注册、无需 API 密钥、无需信用卡登记。 这适用于任何客户端为资源或服务向服务器付款的场景:API 访问、数字内容、商户结账或代理之间的付款。 ### x402 与 MPP x402 是最初的 HTTP-402 支付协议。[机器支付协议 (MPP)](/cn/explanation/mpp) 是一个更广泛的、IETF 标准轨道的后继协议,它增加了支付意图(会话、订阅)、多轨支持(卡、Lightning)以及生产特性(正文摘要绑定、幂等性)。MPP 客户端向后兼容:它们无需更改即可调用 x402 服务器。 如今在 Stable 上,最直接的途径是通过 Semantic Pay 或 Heurist 使用 x402。要在同一 USDT0 轨道上使用 MPP 的传输格式,请参阅[在 Stable 上构建 MPP 端点](/cn/how-to/build-mpp-endpoint)。 ### 它解决了什么问题? 如今在互联网上为服务付款需要在每一步都进行用户干预:注册账户、登录、登记付款方式。这种模式无法扩展到: * 太小而不足以证明基础设施成本合理的服务 * 太便宜而无法承担卡网络费用的交易 * 无法执行注册流程的自主代理(AI、机器人、IoT 设备) 有了 x402,客户端只需要一个钱包即可付款。 | **方面** | **传统计费** | **使用 x402** | | :-------- | :------------- | :-------------- | | 需要账户 | 是 | 否 | | 需要 API 密钥 | 是 | 否 | | 最低可行价格 | \~$0.30(卡处理下限) | \~$0.001(链上) | | 结算时间 | 数天(卡网络) | 亚秒级(在 Stable 上) | | 需要 PCI 合规 | 是 | 否 | ### 工作原理 #### 三种角色 **客户端**是任何需要资源的一方:网页应用、后端服务、CLI 工具或 AI 代理。客户端只需要一个钱包(一个可以签署 ERC-3009 授权的私钥)。 **服务器**是任何提供资源的一方。服务器通过将 x402 中间件附加到其端点来定义什么收费多少。 **促成方**是结算服务。它接收来自服务器的已签名付款,对其进行验证,提交链上交易,并返回结果。促成方从不持有客户端的资金。转账直接在代币合约内从客户端转移到服务器。 在 Stable 上,[Semantic Pay](https://x402.semanticpay.io) 运营着一个公共促成方。 #### 付款流程 1. **客户端请求资源。** 客户端向服务器发送正常的 HTTP 请求(GET、POST 等)。 2. **服务器返回 402。** 服务器返回 HTTP `402 Payment Required` 以及一个包含客户端所需全部信息的 `PAYMENT-REQUIRED` 标头:需要付多少款、哪种代币、哪个网络以及将资金发送到哪里。 3. **客户端签名并重新提交。** 客户端读取付款要求,为指定金额签署一份 ERC-3009 授权,并使用包含已签名授权的 `PAYMENT-SIGNATURE` 标头重新提交原始请求。 4. **促成方验证并结算。** 服务器将已签名的付款转发给其促成方。促成方验证签名,在链上提交 `transferWithAuthorization` 调用,确认后,服务器返回所请求的资源以及一个包含结算收据的 `PAYMENT-RESPONSE` 标头。 #### 三个标头 所有付款信息都通过标准的 HTTP 标头传输,以 Base64 编码: | **标头** | **方向** | **内容** | | :------------------ | :------ | :------------------------- | | `PAYMENT-REQUIRED` | 服务器到客户端 | 付款方案、代币地址、金额、收款人地址、网络标识符 | | `PAYMENT-SIGNATURE` | 客户端到服务器 | 证明客户端已授权转账的已签名 ERC-3009 授权 | | `PAYMENT-RESPONSE` | 服务器到客户端 | 包括交易哈希和确认状态的结算结果 | 这种设计适用于任何 HTTP 客户端、任何编程语言以及任何支持自定义标头的基础设施。 ### Stable 上的 x402 x402 协议定义了付款如何通过 HTTP 工作。Stable 提供使其在生产环境中实用的结算环境。 #### 亚秒级最终性 Stable 的共识提供亚秒级的区块最终性(约 700 毫秒),使 x402 促成方能够实时验证和结算交易。这对于 AI 代理或 IoT 设备可能快速连续执行许多小额付款的高频自动化交互至关重要。 #### 单一资产结算 在 Stable 上,USDT0 既是原生 gas 代币,也是付款代币。整个 x402 付款生命周期仅在 USDT0 上运行。客户端仅持有 USDT0,促成方使用其结算的同一代币提交交易。对于使用 x402 的 AI 代理而言,这意味着代理钱包只需管理一种资产。 #### 微定价 价格以 USDT0 原子单位(6 位小数)计价:成本参数 `"1000"` 恰好转换为 $0.001。这种精度使 x402 服务器能够将价格设置为一美分的几分之一。 #### Gas 豁免集成 [Gas 豁免](/cn/how-to/integrate-gas-waiver)完全消除了交易成本。x402 促成方可以使用 Gas 豁免基础设施提交 `transferWithAuthorization` 调用,而无需向买方或卖方收取 gas 费用。这意味着 Stable 上的 x402 微支付除了付款金额本身之外不产生任何额外开销。 ### 基础设施 #### Semantic Pay [Semantic Pay](https://x402.semanticpay.io) 为 Stable 提供了一个公共的 x402 促成方。它处理签名验证、链上提交和确认跟踪。在 Stable 上集成 x402 的开发者可以将其中间件指向此端点,而无需运行自己的结算基础设施。 **促成方端点:** `https://x402.semanticpay.io` #### WDK(钱包开发工具包) 为了让 AI 代理自主参与 x402,它们需要能够以编程方式控制的钱包。Tether 的开源 WDK 提供了这一点: * **自我托管**:WDK 使 AI 代理能够在本地生成和存储私钥,而无需依赖中心化的 API 基础设施。 * **x402 兼容性**:WDK 的 `WalletAccountEvm` 实例原生满足 x402 SDK 所需的客户端签名者接口,使代理能够自动拦截 402 HTTP 响应、签署 ERC-3009 授权并重新提交请求。 **另请参阅:** * [ERC-3009(带授权转账)](/cn/explanation/erc-3009):x402 使用的链上结算标准 * [支付用例](/cn/explanation/payment-use-cases-overview):P2P、订阅、发票和 API 计费模式 * [Gas 豁免](/cn/how-to/integrate-gas-waiver):零成本交易提交