diff --git a/cspell.json b/cspell.json index 3cf1a67..edb3392 100644 --- a/cspell.json +++ b/cspell.json @@ -58,6 +58,7 @@ "Hyperlane", "idkit", "IDKit", + "IERC", "ifying", "ijkl", "interchain", diff --git a/docs.json b/docs.json index 72b69ff..3b05f4a 100644 --- a/docs.json +++ b/docs.json @@ -38,6 +38,88 @@ } ] }, + { + "tab": "World ID", + "groups": [ + { + "group": "Overview", + "pages": ["world-id/overview"] + }, + { + "group": "IDKit", + "pages": [ + "world-id/idkit/integrate", + "world-id/idkit/build-with-llms", + "world-id/idkit/onchain-verification", + "world-id/idkit/design-guidelines", + { + "group": "SDK Reference", + "pages": [ + "world-id/idkit/javascript", + "world-id/idkit/react", + "world-id/idkit/swift", + "world-id/idkit/kotlin", + "world-id/idkit/go", + "world-id/idkit/error-codes" + ] + } + ] + }, + { + "group": "Credentials", + "pages": ["world-id/credentials/legacy-presets"] + }, + { + "group": "Migration", + "pages": ["world-id/4-0-migration"] + }, + { + "group": "Selfie Check (Beta)", + "hidden": true, + "pages": ["world-id/selfie-check/overview"] + }, + { + "group": "Technical Reference", + "pages": [ + "world-id/concepts", + "world-id/reference/authenticator", + { + "group": "API References", + "pages": [ + { + "group": "Indexer", + "openapi": "https://indexer.us.id-infra.worldcoin.dev/openapi.json", + "pages": [ + "POST /inclusion-proof", + "POST /packed-account", + "POST /signature-nonce", + "POST /authenticator-pubkeys" + ] + }, + { + "group": "Gateway", + "openapi": "https://gateway.id-infra.worldcoin.dev/openapi.json", + "pages": [ + "POST /create-account", + "POST /insert-authenticator", + "POST /update-authenticator", + "POST /remove-authenticator", + "POST /recover-account", + "GET /is-valid-root", + "GET /status/{id}" + ] + }, + { + "group": "Issuers", + "pages": ["world-id/reference/poh-issuer"] + } + ] + }, + "world-id/reference/contracts" + ] + } + ] + }, { "tab": "Mini Apps", "groups": [ @@ -131,88 +213,6 @@ } ] }, - { - "tab": "World ID", - "groups": [ - { - "group": "Overview", - "pages": ["world-id/overview"] - }, - { - "group": "IDKit", - "pages": [ - "world-id/idkit/integrate", - "world-id/idkit/build-with-llms", - "world-id/idkit/onchain-verification", - "world-id/idkit/design-guidelines", - { - "group": "SDK Reference", - "pages": [ - "world-id/idkit/javascript", - "world-id/idkit/react", - "world-id/idkit/swift", - "world-id/idkit/kotlin", - "world-id/idkit/go", - "world-id/idkit/error-codes" - ] - } - ] - }, - { - "group": "Credentials", - "pages": ["world-id/credentials/legacy-presets"] - }, - { - "group": "Migration", - "pages": ["world-id/4-0-migration"] - }, - { - "group": "Selfie Check (Beta)", - "hidden": true, - "pages": ["world-id/selfie-check/overview"] - }, - { - "group": "Technical Reference", - "pages": [ - "world-id/concepts", - "world-id/reference/authenticator", - { - "group": "API References", - "pages": [ - { - "group": "Indexer", - "openapi": "https://indexer.us.id-infra.worldcoin.dev/openapi.json", - "pages": [ - "POST /inclusion-proof", - "POST /packed-account", - "POST /signature-nonce", - "POST /authenticator-pubkeys" - ] - }, - { - "group": "Gateway", - "openapi": "https://gateway.id-infra.worldcoin.dev/openapi.json", - "pages": [ - "POST /create-account", - "POST /insert-authenticator", - "POST /update-authenticator", - "POST /remove-authenticator", - "POST /recover-account", - "GET /is-valid-root", - "GET /status/{id}" - ] - }, - { - "group": "Issuers", - "pages": ["world-id/reference/poh-issuer"] - } - ] - }, - "world-id/reference/contracts" - ] - } - ] - }, { "tab": "Agents", "groups": [ diff --git a/images/docs/mini-apps/commands/permit2-whitelist.png b/images/docs/mini-apps/commands/permit2-whitelist.png new file mode 100644 index 0000000..e0e8d62 Binary files /dev/null and b/images/docs/mini-apps/commands/permit2-whitelist.png differ diff --git a/index.mdx b/index.mdx index 6c56419..464f99b 100644 --- a/index.mdx +++ b/index.mdx @@ -8,12 +8,12 @@ description: "Build Mini Apps, integrate World ID, and deploy on World Chain wit --- - - Mini applications distributed in World App - Anonymous proof of human for your website or app + + Mini applications distributed in World App + Distinguish human-backed agents from bots and scripts diff --git a/mini-apps/commands/send-transaction.mdx b/mini-apps/commands/send-transaction.mdx index cf67717..6b5ff2a 100644 --- a/mini-apps/commands/send-transaction.mdx +++ b/mini-apps/commands/send-transaction.mdx @@ -5,52 +5,122 @@ description: "Send one or more World Chain transactions using the unified MiniKi "twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" --- -Use `MiniKit.sendTransaction()` to submit one or more World Chain transactions. +Use `MiniKit.sendTransaction()` to submit one or more World Chain transactions. -## Basic Usage +**Breaking Changes in MiniKit v2**: This command now uses Permit2 AllowanceTransfers under the hood. +You will no longer need to specify a permit2 array, but will need to bundle an approval to the permit2 contract in the same transaction as your main action. + +## Permit2 Allowance Transfers + +[Allowance transfers](https://docs.uniswap.org/contracts/permit2/reference/allowance-transfer#approve) are the default method for moving tokens in mini apps. +You should only rely on the `approve` method for Allowance Transfers, and can bundle them in one sendTransaction call with your main contract action for a seamless user experience. -MiniKit uses Permit2 under the hood. -```tsx title="Example" +```tsx title="Frontend" import { MiniKit } from "@worldcoin/minikit-js"; -import type { - CommandResultByVia, - MiniKitSendTransactionOptions, - SendTransactionResult, -} from "@worldcoin/minikit-js/commands"; - -export async function sendTransaction() { - const input = { - // Set to 480 for World Chain +import { encodeFunctionData, parseEther } from "viem"; + +const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; + +async function approveAndSwap( + token: `0x${string}`, + spender: `0x${string}`, + amount: bigint, +) { + const result = await MiniKit.sendTransaction({ chainId: 480, transactions: [ + // 1. Approve spender via Permit2 { - to: "0x9Cf4F011F55Add3ECC1B1B497A3e9bd32183D6e8", - data: "0x1234", + to: PERMIT2, + data: encodeFunctionData({ + abi: [ + { + name: "approve", + type: "function", + inputs: [ + { name: "token", type: "address" }, + { name: "spender", type: "address" }, + { name: "amount", type: "uint160" }, + { name: "expiration", type: "uint48" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + ], + functionName: "approve", + args: [ + token, + spender, + amount, + // 30-day expiration + Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, + ], + }), + }, + // 2. Call your contract (which pulls tokens via permit2.transferFrom) + { + to: spender, + data: encodeFunctionData({ + abi: [ + { + name: "swap", + type: "function", + inputs: [{ name: "amount", type: "uint256" }], + outputs: [], + stateMutability: "nonpayable", + }, + ], + functionName: "swap", + args: [amount], + }), }, ], - } satisfies MiniKitSendTransactionOptions; - - const result: CommandResultByVia = - await MiniKit.sendTransaction(input); + }); console.log(result.data.userOpHash); } ``` -```ts title="Type" -type MiniKitSendTransactionOptions = { - chainId: number; - transactions: { - to: string; - data?: string; - value?: string; - }[]; - fallback?: () => unknown; -}; +```solidity title="Contract" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + +contract Swap { + IERC20 public tokenA; + IERC20 public tokenB; + IAllowanceTransfer public immutable permit2; + + constructor(address _tokenA, address _tokenB, address _permit2) { + tokenA = IERC20(_tokenA); + tokenB = IERC20(_tokenB); + permit2 = IAllowanceTransfer(_permit2); + } + + function swap(uint256 amount) external { + // Pull tokenA from caller via Permit2 AllowanceTransfer + permit2.transferFrom( + msg.sender, + address(this), + uint160(amount), + address(tokenA) + ); + // Send tokenB to caller + tokenB.transfer(msg.sender, amount); + } +} ``` + + World App automatically approves new ERC-20 tokens to the Permit2 contract. + Your contract only needs to call `permit2.transferFrom` — the token-level + approval is already in place. + + ## Result @@ -99,12 +169,12 @@ type SendTransactionResponse = The command resolves as soon as the user operation is submitted, so the first identifier you receive is `userOpHash`. -You can either use `@worldcoin/minikit-react` hook or poll the Developer Portal +You can either use the `@worldcoin/minikit-react` hook or poll the Developer Portal to check when the user operation is mined and get the final `transaction_hash`. ```tsx title="React" -import { useWaitForUserOperationReceipt } from "@worldcoin/minikit-react"; +import { useUserOperationReceipt } from "@worldcoin/minikit-react"; import { createPublicClient, http } from "viem"; import { worldchain } from "viem/chains"; @@ -113,16 +183,13 @@ const client = createPublicClient({ transport: http("https://worldchain-mainnet.g.alchemy.com/public"), }); -const { - isLoading, - isSuccess, - isError, - transactionHash, - receipt, -} = useWaitForUserOperationReceipt({ - client, - userOpHash, -}); +const { poll, isLoading, reset } = useUserOperationReceipt({ client }); + +const onClick = async () => { + const result = await MiniKit.sendTransaction({...}); + const { receipt } = await poll(result.data.userOpHash); + // receipt contains the final transaction receipt +}; ``` ```ts title="API" @@ -169,7 +236,29 @@ for the full endpoint response shape. ## Fallback Behavior -By default we intend for mini apps to be useable outside of World App. Fallbacks generally will not be needed and you should instead follow the [migration path outlined](/mini-apps/more/minikit-v2). +By default we intend for mini apps to be useable outside of World App. Fallbacks generally will not be needed for this command and you should instead follow the [migration path outlined](/mini-apps/more/minikit-v2). + +## Allowlisting Contracts and Tokens + +Before your mini app can send transactions, you must allowlist the contracts and tokens it interacts with. Navigate to **Developer Portal > Mini App > Permissions** and add: + +- **Permit2 Tokens** — every ERC-20 token your app transfers via Permit2 +- **Contract Entrypoints** — every contract your app calls directly + +
+ Developer Portal showing Permit2 token and contract entrypoint whitelisting +

Developer Portal Whitelist

+
+ + + Transactions that touch non-whitelisted contracts or tokens will be blocked by + the backend with an `invalid_contract` error. + ## Preview diff --git a/mini-apps/quick-start/init.mdx b/mini-apps/quick-start/init.mdx index b0c500f..0208eec 100644 --- a/mini-apps/quick-start/init.mdx +++ b/mini-apps/quick-start/init.mdx @@ -6,66 +6,62 @@ title: Initialization When your mini app initializes inside World App, MiniKit stores device context, launch context, and basic user metadata on the client. -Initialize MiniKit before calling any command: +If you are using React, `MiniKitProvider` will perform the initialization for you. +Otherwise, manually initialize MiniKit at the start of your app: ```tsx import { MiniKit } from "@worldcoin/minikit-js"; const { success } = MiniKit.install(); - -if (!success) { - console.error("MiniKit is unavailable in this environment"); -} ``` -If you are using React, `MiniKitProvider` calls `MiniKit.install()` for you. ## MiniKit State After install, these are the public MiniKit state accessors you can rely on: -```tsx -type MiniKitState = { - user: { - walletAddress?: string; - username?: string; - profilePictureUrl?: string; - permissions?: { - notifications: boolean; - contacts: boolean; - }; - optedIntoOptionalAnalytics?: boolean; - }; - deviceProperties: { - safeAreaInsets?: { - top: number; - right: number; - bottom: number; - left: number; - }; - deviceOS?: string; - worldAppVersion?: number; - }; - location: "chat" | "home" | "app-store" | "deep-link" | "wallet-tab" | null; -}; -``` - -Example: - -```tsx -import { MiniKit } from "@worldcoin/minikit-js"; - -console.log(MiniKit.user.optedIntoOptionalAnalytics); -console.log(MiniKit.deviceProperties.safeAreaInsets); -console.log(MiniKit.deviceProperties.deviceOS); -console.log(MiniKit.deviceProperties.worldAppVersion); -console.log(MiniKit.location); -``` + + + ```tsx + // MiniKit state + { + user: { + walletAddress?: string; + username?: string; + profilePictureUrl?: string; + permissions?: { + notifications: boolean; + contacts: boolean; + }; + optedIntoOptionalAnalytics?: boolean; + verificationStatus?: { + isOrbVerified: boolean; + isDocumentVerified: boolean; + isSecureDocumentVerified: boolean; + }; + preferredCurrency?: string; + pendingNotifications?: number; + }; + deviceProperties: { + safeAreaInsets?: { + top: number; + right: number; + bottom: number; + left: number; + }; + deviceOS?: string; + worldAppVersion?: number; + }; + location: "chat" | "home" | "app-store" | "deep-link" | "wallet-tab" | null; + } + ``` + + Notes: -- `MiniKit.user.optedIntoOptionalAnalytics` is available during initialization. -- `walletAddress`, `username`, and `profilePictureUrl` are usually populated later, for example after `walletAuth()`. +- `walletAddress`, `verificationStatus`, `preferredCurrency`, `pendingNotifications`, and `optedIntoOptionalAnalytics` are all available at initialization. +- `username` and `profilePictureUrl` are populated after `walletAuth()`. - `MiniKit.location` is the mapped launch location. Use this instead of reading older launch-origin fields directly. ## Permissions @@ -81,10 +77,6 @@ import type { MiniAppGetPermissionsSuccessPayload } from "@worldcoin/minikit-js/ const result = await MiniKit.getPermissions(); const permissions: MiniAppGetPermissionsSuccessPayload["permissions"] = result.data.permissions; - -console.log(permissions.notifications); -console.log(permissions.contacts); -console.log(permissions.microphone); ``` Notes: @@ -97,92 +89,127 @@ Notes: MiniKit normalizes the raw World App launch origin into: -```tsx -type MiniAppLaunchLocation = - | "chat" - | "home" - | "app-store" - | "deep-link" - | "wallet-tab" - | null; -``` - -Example: - -```tsx -import { MiniKit } from "@worldcoin/minikit-js"; - -if (MiniKit.location === "chat") { - console.log("Opened from chat"); -} -``` + + + ```tsx + type MiniAppLaunchLocation = + | "chat" + | "home" + | "app-store" + | "deep-link" + | "wallet-tab" + | null; + ``` + + + + ```tsx + import { MiniKit } from "@worldcoin/minikit-js"; + + if (MiniKit.location === "chat") { + console.log("Opened from chat"); + } + ``` + + ## Raw World App Object If you need the untransformed World App payload, read `window.WorldApp` directly. -```tsx -type WorldAppContext = { - world_app_version: number; - device_os: "ios" | "android"; - is_optional_analytics: boolean; - supported_commands: Array<{ - name: - | "attestation" - | "pay" - | "wallet-auth" - | "send-transaction" - | "sign-message" - | "sign-typed-data" - | "share-contacts" - | "request-permission" - | "get-permissions" - | "send-haptic-feedback" - | "share" - | "chat" - | "close-miniapp"; - supported_versions: number[]; - }>; - safe_area_insets: { - top: number; - right: number; - bottom: number; - left: number; - }; - location: string | null | undefined; -}; -``` - -Example: - -```json -{ - "world_app_version": 4000301, - "device_os": "ios", - "is_optional_analytics": true, - "supported_commands": [ - { "name": "attestation", "supported_versions": [1] }, - { "name": "pay", "supported_versions": [1] }, - { "name": "wallet-auth", "supported_versions": [2] }, - { "name": "send-transaction", "supported_versions": [2] }, - { "name": "sign-message", "supported_versions": [1] }, - { "name": "sign-typed-data", "supported_versions": [1] }, - { "name": "share-contacts", "supported_versions": [1] }, - { "name": "request-permission", "supported_versions": [1] }, - { "name": "get-permissions", "supported_versions": [1] }, - { "name": "send-haptic-feedback", "supported_versions": [1] }, - { "name": "share", "supported_versions": [1] }, - { "name": "chat", "supported_versions": [1] }, - { "name": "close-miniapp", "supported_versions": [1] } - ], - "safe_area_insets": { - "top": 0, - "right": 0, - "bottom": 0, - "left": 0 - }, - "location": "deep-link" -} -``` + + + ```tsx + // window.WorldApp + { + world_app_version: number; + device_os: "ios" | "android"; + is_optional_analytics: boolean; + wallet_address: string; + verification_status: { + is_orb_verified: boolean; + is_document_verified: boolean; + is_secure_document_verified: boolean; + }; + preferred_currency: string; + pending_notifications: number; + supported_commands: Array<{ + name: + | "verify" + | "attestation" + | "pay" + | "wallet-auth" + | "send-transaction" + | "sign-message" + | "sign-typed-data" + | "share-contacts" + | "request-permission" + | "get-permissions" + | "send-haptic-feedback" + | "share" + | "chat" + | "close-miniapp" + | "microphone-stream-started" + | "microphone-stream-ended"; + supported_versions: number[]; + }>; + safe_area_insets: { + top: number; + right: number; + bottom: number; + left: number; + }; + location: { + open_origin: string; + } | null | undefined; + } + ``` + + + + ```json + { + "world_app_version": 4001000, + "device_os": "ios", + "is_optional_analytics": true, + "wallet_address": "0x377da9cab87c04a1d6f19d8b4be9aef8df26fcdd", + "verification_status": { + "is_orb_verified": true, + "is_document_verified": true, + "is_secure_document_verified": false + }, + "preferred_currency": "USD", + "pending_notifications": 0, + "supported_commands": [ + { "name": "verify", "supported_versions": [1] }, + { "name": "attestation", "supported_versions": [1] }, + { "name": "pay", "supported_versions": [1] }, + { "name": "wallet-auth", "supported_versions": [1, 2] }, + { "name": "send-transaction", "supported_versions": [1, 2] }, + { "name": "sign-message", "supported_versions": [1] }, + { "name": "sign-typed-data", "supported_versions": [1] }, + { "name": "share-contacts", "supported_versions": [1] }, + { "name": "request-permission", "supported_versions": [1] }, + { "name": "get-permissions", "supported_versions": [1] }, + { "name": "send-haptic-feedback", "supported_versions": [1] }, + { "name": "share", "supported_versions": [1] }, + { "name": "chat", "supported_versions": [1] }, + { "name": "close-miniapp", "supported_versions": [1] }, + { "name": "microphone-stream-started", "supported_versions": [1] }, + { "name": "microphone-stream-ended", "supported_versions": [1] } + ], + "safe_area_insets": { + "top": 0, + "right": 0, + "bottom": 0, + "left": 0 + }, + "location": { + "open_origin": "deeplink" + } + } + ``` + + Use `window.WorldApp` only when you need the raw payload. In application code, prefer `MiniKit.user`, `MiniKit.deviceProperties`, and `MiniKit.location`.