diff --git a/.env.example b/.env.example index fe78808..94d1a29 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,153 @@ -EXPO_PUBLIC_WDK_INDEXER_BASE_URL=https://wdk-api.tether.io -EXPO_PUBLIC_WDK_INDEXER_API_KEY=PUT_WDK_API_KEY_HERE -EXPO_PUBLIC_TRON_API_KEY=PUT_HERE -EXPO_PUBLIC_TRON_API_SECRET=PUT_HERE \ No newline at end of file +# ══════════════════════════════════════════════════════════════════════════ +# Copy this file to .env and fill in what you need. The app boots fine with +# NONE of these set — a given network/feature just won't work until it's +# configured. See docs/CLOUD_BACKUP.md and docs/ENVIRONMENT.md for the full +# setup walkthroughs referenced throughout this file. +# ══════════════════════════════════════════════════════════════════════════ + +# ── Bitcoin ────────────────────────────────────────────────────────────── +# A Blockbook-compatible HTTP endpoint. IMPORTANT: must include the /api +# suffix — the client does plain string concatenation with no normalization, +# so a base URL without /api silently hits the wrong path and gets back an +# HTML error page instead of JSON. +# Testnet3 (safe, free, no sign-up): https://tbtc1.trezor.io/api +# Mainnet: https://btc1.trezor.io/api +EXPO_PUBLIC_BTC_PROVIDER=https://tbtc1.trezor.io/api +# +# Only set this when EXPO_PUBLIC_BTC_PROVIDER points at a testnet — leave +# blank/unset for real mainnet. This is what makes the app correctly say +# "Bitcoin (Testnet)" vs plain "Bitcoin" everywhere a network name is shown, +# matching whatever's ACTUALLY configured rather than a hardcoded guess. +EXPO_PUBLIC_BTC_NETWORK_LABEL=Testnet +# If you ever repoint EITHER provider to real mainnet, just clear the +# matching *_NETWORK_LABEL var (leave it blank) — the display label updates +# automatically, no code change needed. + + +# ── Ethereum family (EIP-7702 gasless) ────────────────────────────────────── +# Three separate networks, each needing its own provider/bundler/paymaster — +# genuinely different chains, not variants of one config. All use +# @tetherto/wdk-wallet-evm-7702-gasless. +# +# Bundler/paymaster: Pimlico and Candide both support EIP-7702 +# UserOperations — see each provider's own dashboard for a free/testnet API +# key. Omit *_PAYMASTER_URL entirely if your bundler URL already serves both +# roles (true for both Pimlico and Candide) — don't duplicate the same URL +# in both fields. +# +# "Ethereum" here means Sepolia testnet by team decision — see the note +# below. Arbitrum and Polygon are real mainnet, by the same decision (their +# testnets, Arbitrum Sepolia and Polygon Amoy, were evaluated and explicitly +# ruled out — see docs/WDK_INTEGRATION.md). + +# Ethereum — Sepolia testnet (chain 11155111). Genuinely free to test with — +# no real funds or gas costs involved, safe to experiment on freely. +EXPO_PUBLIC_EVM_ETHEREUM_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com +EXPO_PUBLIC_EVM_ETHEREUM_BUNDLER_URL=https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_KEY +EXPO_PUBLIC_EVM_ETHEREUM_PAYMASTER_URL=https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_KEY +# +# Same idea — only set this when the Ethereum provider above points at a +# testnet. Leave blank/unset for real mainnet. +EXPO_PUBLIC_EVM_ETHEREUM_NETWORK_LABEL=Sepolia +# If you ever repoint EITHER provider to real mainnet, just clear the +# matching *_NETWORK_LABEL var (leave it blank) — the display label updates +# automatically, no code change needed. + +# Arbitrum One — ⚠️ REAL MAINNET, REAL MONEY. Gas sponsorship here is NOT +# free the way Sepolia is; it draws on your bundler provider's real +# balance/credits the moment you send something. Using Arbitrum's own +# official public RPC (no rate-limit surprises like drpc.org's free tier). +EXPO_PUBLIC_EVM_ARBITRUM_PROVIDER=https://arb1.arbitrum.io/rpc +EXPO_PUBLIC_EVM_ARBITRUM_BUNDLER_URL=https://api.pimlico.io/v2/arbitrum/rpc?apikey=YOUR_PIMLICO_KEY +EXPO_PUBLIC_EVM_ARBITRUM_PAYMASTER_URL=https://api.pimlico.io/v2/arbitrum/rpc?apikey=YOUR_PIMLICO_KEY + +# Polygon PoS — ⚠️ REAL MAINNET, REAL MONEY, same caution as Arbitrum above. +# STILL on drpc.org's free tier as of this writing — this project has +# already hit drpc.org's per-request batch-size limit once (see +# docs/TROUBLESHOOTING.md), and this network hasn't been moved off it yet. +# If you see repeated timeout/failure patterns specifically on Polygon, +# this is the first thing to suspect — consider a dedicated provider +# (Alchemy/Infura) instead, especially for any real, sustained usage. +EXPO_PUBLIC_EVM_POLYGON_PROVIDER=https://polygon.drpc.org +EXPO_PUBLIC_EVM_POLYGON_BUNDLER_URL=https://api.pimlico.io/v2/polygon/rpc?apikey=YOUR_PIMLICO_KEY +EXPO_PUBLIC_EVM_POLYGON_PAYMASTER_URL=https://api.pimlico.io/v2/polygon/rpc?apikey=YOUR_PIMLICO_KEY + +# Optional — a different smart-account implementation than the package's +# own documented SimpleAccount example (used by default if left blank). +EXPO_PUBLIC_EVM_DELEGATION_ADDRESS= + + +# ── Pricing (CoinGecko) ──────────────────────────────────────────────────── +# Powers the real USD total/fiat values on Home, Review, and Activity. +# Works WITHOUT a key (CoinGecko's public host, rate-limited). Add a key +# only if you hit rate limits: +# https://docs.coingecko.com/docs/setting-up-your-api-key +EXPO_PUBLIC_COINGECKO_API_KEY= + + +# ── Transaction history (WDK Indexer API) ────────────────────────────────── +# Powers the Activity tab. REQUIRED — nothing shows up there without a real, +# registered key. Free registration: https://wdk-api.tether.io/register +# +# Two real, structural limitations (not bugs — see docs/WDK_INTEGRATION.md): +# - Native ETH has NO transaction history available through this API at +# all (its Token type is exactly "usdt" | "xaut" | "btc" — there's no +# "eth"). ETH is excluded from Activity results, not silently shown +# as empty. +# - This API treats "sepolia" as a distinct blockchain from "ethereum" — +# since EXPO_PUBLIC_EVM_ETHEREUM_* above actually points at Sepolia, +# that mapping is handled internally (src/wdk/indexer.ts), but it WOULD +# need updating if Ethereum is ever repointed to real mainnet. +EXPO_PUBLIC_WDK_INDEXER_API_KEY= + + +# ── Tron — currently ON HOLD, not wired up ──────────────────────────────── +# gasfree.io is mid-migration to a new API version; wdk-wallet-tron-gasfree +# isn't in the compiled worklet right now (see wdk.config.js). No env vars +# needed until it's re-added — this section intentionally left empty. + + +# ── Cloud Backup — iOS CloudKit ─────────────────────────────────────────── +# See docs/CLOUD_BACKUP.md for the full Apple Developer Portal walkthrough. +# None of these are required for the app to run — only for the iCloud +# backup option specifically. + +# Format: iCloud. — use YOUR app's actual bundle +# identifier (io.tether.wdkstarterreactnative by default), not a container +# borrowed from another app. A shared/reused container risks the wallet-ID +# collision issue documented in docs/ARCHITECTURE.md. +EXPO_PUBLIC_CLOUDKIT_CONTAINER_ID=iCloud.io.tether.wdkstarterreactnative + +# From icloud.developer.apple.com/dashboard → API Tokens. Type: "User +# authentication token (Web Services)". Sign in Callback MUST be "URL +# Redirect", not "Post Message" — the latter silently doesn't work with +# this app's WebView-based sign-in. +EXPO_PUBLIC_CLOUDKIT_API_TOKEN= + +# Any URL you choose — doesn't need to be real/reachable, since this app +# intercepts the redirect before it loads. Must match, character for +# character, the Sign in Callback URL Redirect registered for the token above. +EXPO_PUBLIC_CLOUDKIT_CALLBACK_URL= + + +# ── Cloud Backup — Google Drive (iOS + Android) ─────────────────────────── +# See docs/CLOUD_BACKUP.md for the full Google Cloud Console walkthrough +# (three separate OAuth clients: Android, iOS, Web). + +# Web Application OAuth Client ID — project-scoped, not app-scoped, so it +# can be reused across multiple apps in the same Google Cloud project. +# +# The value below is a PUBLISHED DEMO web client — usable as-is for local +# development. DEMO ONLY: do not ship this in a production build; set up +# your own via docs/CLOUD_BACKUP.md. +EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=518914426511-um41gv98teak4u327nablql8bpb4vonb.apps.googleusercontent.com + +# iOS OAuth Client ID — a SEPARATE credential from the Web client above +# (bound to your iOS bundle identifier, not a SHA-1). Needed because this +# app lets a person choose Google Drive on iOS too, not just Android. Also +# requires the reversed client ID registered as a URL scheme in app.json's +# google-signin plugin config — see docs/CLOUD_BACKUP.md section 3, or +# sign-in fails silently on iOS with no useful error. +# +# Pairs with the demo Web client above — same DEMO ONLY caveat applies. +EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=518914426511-c46v1j9olhu5trra830bfo13cdj3ge4h.apps.googleusercontent.com diff --git a/.gitignore b/.gitignore index 2241f17..480185f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,21 @@ app-example # E2E tests test/apps/ +node_modules/ +.expo/ +dist/ +ios/ +android/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +*.bundle +*.bundle.js +web-build/ +.wdk-bundle/ +.wdk/ +.env \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index cfe3e5e..0000000 --- a/.prettierignore +++ /dev/null @@ -1,59 +0,0 @@ -# Dependencies -node_modules/ -package-lock.json -yarn.lock - -# Build outputs -dist/ -build/ -.expo/ - -# Native build outputs -ios/ -android/ - -# Generated files -src/worklet.bundle.js -*.bundle.js - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ - -# Metro -.metro-health-check* - -# Expo -.expo-shared/ - -# VSCode -.vscode/ - -# IDE -.idea/ -*.swp -*.swo - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Temporary files -tmp/ -temp/ \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 43f60c2..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - // React Native and TypeScript friendly settings - semi: true, - trailingComma: 'es5', - singleQuote: true, - printWidth: 100, - tabWidth: 2, - useTabs: false, - quoteProps: 'as-needed', - jsxSingleQuote: false, - bracketSpacing: true, - bracketSameLine: false, - arrowParens: 'always', - endOfLine: 'lf', - - // File-specific overrides - overrides: [ - { - files: '*.json', - options: { - printWidth: 80, - }, - }, - { - files: '*.md', - options: { - printWidth: 80, - proseWrap: 'always', - }, - }, - ], -}; diff --git a/assets/docs/demo.mp4 b/.wdk-bundle/wdk-worklet.bundle.js similarity index 55% rename from assets/docs/demo.mp4 rename to .wdk-bundle/wdk-worklet.bundle.js index 291ae4b..d942bc2 100644 Binary files a/assets/docs/demo.mp4 and b/.wdk-bundle/wdk-worklet.bundle.js differ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 45d257b..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,133 +0,0 @@ - -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 8110ea1..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,98 +0,0 @@ -# Contributing - -Contributions are always welcome, no matter how large or small! - -We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). - -## Development workflow - -This repository contains a single React Native UI library package. - -To get started, make sure you have the correct version of [Node.js](https://nodejs.org/) installed. See the [`.nvmrc`](./.nvmrc) file for the version used in this project. - -Install dependencies in the root directory: - -```sh -npm install -``` - -Make sure your code passes TypeScript and ESLint. Run the following to verify: - -```sh -npm run typecheck -npm run lint -``` - -To fix formatting errors, run the following: - -```sh -npm run lint -- --fix -``` - -Remember to add tests for your change if possible. Run the unit tests by: - -```sh -npm test -``` - -### Commit message convention - -We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: - -- `fix`: bug fixes, e.g. fix crash due to deprecated method. -- `feat`: new features, e.g. add new method to the module. -- `refactor`: code refactor, e.g. migrate from class components to hooks. -- `docs`: changes into documentation, e.g. add usage example for the module. -- `test`: adding or updating tests, e.g. add integration tests using detox. -- `chore`: tooling changes, e.g. change CI config. - -Our pre-commit hooks verify that your commit message matches this format when committing. - -### Linting and tests - -[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) - -We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. - -Our pre-commit hooks verify that the linter and tests pass when committing. - -### Publishing to npm - -Maintainers can publish a new version to npm using standard npm commands. Ensure you are logged in with the correct account and have publish rights. - -1. Bump the version (choose one): - - Patch: `npm version patch` - - Minor: `npm version minor` - - Major: `npm version major` - -2. Build the package (runs via prepare): - ```sh - npm run prepare - ``` - -3. Publish: - ```sh - npm publish --access public - ``` - -### Scripts - -The `package.json` file contains various scripts for common tasks: - -- `npm install`: setup project by installing dependencies. -- `npm run typecheck`: type-check files with TypeScript. -- `npm run lint`: lint files with ESLint. -- `npm test`: run unit tests with Jest. -- `npm run prepare`: build the library using Bob. - -### Sending a pull request - -> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). - -When you're sending a pull request: - -- Prefer small pull requests focused on one change. -- Verify that linters and tests are passing. -- Review the documentation to make sure it looks good. -- Follow the pull request template when opening a pull request. -- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. diff --git a/README.md b/README.md index 7d9c2b5..6ab46f7 100644 --- a/README.md +++ b/README.md @@ -1,440 +1,127 @@ -# @tetherto/wdk-starter-react-native - -> [!WARNING] -> **Alpha Software** -> -> This starter template is currently in **alpha** stage and under active development. It may contain bugs, incomplete features, and breaking changes. **Do not use in production environments or with real funds.** -> -> For production use, please wait for the stable release or use at your own risk in development/testing environments only. - -An Expo + React Native starter demonstrating how to build a secure, multi-chain, non-custodial wallet using the WDK (Wallet Development Kit). Features BareKit worklets for cryptographic operations, secure secret management, and a complete wallet implementation with onboarding, transactions, and asset management. - -Click below to see the wallet in action: - -[![Demo Video](assets/docs/demo-thumbnail.png)](assets/docs/demo.mp4) - -## 🔍 About WDK - -This repository is part of the [**WDK (Wallet Development Kit)**](https://wallet.tether.io/) project, which empowers developers to build secure, non-custodial wallets with unified blockchain access, stateless architecture, and complete user control. - -For detailed documentation about the complete WDK ecosystem, visit [docs.wallet.tether.io](https://docs.wallet.tether.io). - -## 🌟 Features - -### Multi-Chain Support -- **Bitcoin (SegWit)**: Native bitcoin transfers with SegWit addresses -- **Ethereum**: EVM transactions with gas sponsorship support -- **Polygon**: Low-cost EVM transactions with gas sponsorship -- **Arbitrum**: Layer 2 scaling with gas sponsorship support -- **TON**: Native TON blockchain transfers -- **Tron**: TRC-20 token support with low fees -- **Solana**: High-performance blockchain support - -### Multi-Token Support -- **BTC**: Native Bitcoin on SegWit and Lightning networks -- **USD₮ (Tether USD)**: Multi-chain USD₮ support (Ethereum, Polygon, Arbitrum, TON, Tron, Solana) -- **XAU₮ (Tether Gold)**: Gold-backed stablecoin on Ethereum - -### Wallet Management -- **Secure Seed Generation**: Cryptographically secure 12-word mnemonic generation -- **Seed Import**: Import existing BIP39-compatible mnemonic phrases -- **Encrypted Storage**: Secure key storage via native secure storage (iOS Keychain, Android KeyStore) -- **Biometric Authentication**: Face ID/Touch ID for wallet unlock -- **Wallet Naming**: Custom wallet names for better organization - -### Asset Management -- **Real-Time Balances**: Live balance updates via [WDK Indexer](https://docs.wallet.tether.io/) -- **Transaction History**: Complete transaction tracking and history -- **Price Conversion**: Real-time fiat pricing via Bitfinex integration -- **Multi-Asset View**: Aggregate portfolio view across all tokens and chains -- **Asset Details**: Detailed views for individual tokens with transaction history - -### User Experience -- **QR Code Scanner**: Scan wallet addresses and payment requests via camera -- **Send Flows**: Complete send flow with network and token selection -- **Receive Flows**: Generate QR codes for receiving payments -- **Activity Feed**: Real-time transaction monitoring with status updates -- **Settings**: Wallet management, security, and app preferences -- **Dark Mode**: Modern dark theme optimized for readability - -## 🧱 Platform Prerequisites - -- Node.js 22+ -- iOS: Xcode toolchain -- Android: SDK (see `app.json` build properties for version requirements) - -## ⬇️ Installation - -Clone this repository and install dependencies: +# WDK Starter Wallet (React Native) + +> ⚠️ **Alpha software.** Under active development. Do not use with real funds +> until a stable release is tagged. + +A reference **non-custodial wallet** built with Expo + React Native on +Tether's [WDK (Wallet Development Kit)](https://wallet.tether.io/). This +repo exists to teach other developers, end to end, how to integrate WDK into +a real app — every architectural decision, and every hard-won bug fix along +the way, is documented rather than hidden. + +If you're new here, **read the docs in this order**: + +1. [`docs/ENVIRONMENT.md`](docs/ENVIRONMENT.md) — toolchain versions that + actually work. Read this *before* running `npm install`. +2. [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — how the app is put + together and why. +3. [`docs/WDK_INTEGRATION.md`](docs/WDK_INTEGRATION.md) — the WDK + integration itself, step by step. +4. [`docs/SECURITY.md`](docs/SECURITY.md) — the password/lock architecture. +5. [`docs/CLOUD_BACKUP.md`](docs/CLOUD_BACKUP.md) — iCloud/Google Drive + backup, including the external account setup you'll need to do yourself. +6. [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — a running list of + real bugs we hit and how they were fixed, so you don't rediscover them. + +## What's actually built right now + +This PR covers the full core wallet experience end to end, against real +WDK + real external services — not mocked, not a design approximation of +the prototype but a direct match against its actual markup/CSS: + +**Onboarding & security** +- Welcome → create or import a wallet; recovery phrase generation/reveal + and import, both with a 12-or-24-word choice and paste-distribution + across the word grid +- App password creation, backed by real envelope encryption (not just a + UI field) +- Cloud backup: the person chooses **either** iCloud **or** Google Drive, + on **either** platform — encrypted with their own app password, not a + fixed passphrase +- App lock/unlock, tested and fixed across iOS/Android's different + background-transition behavior + +**Wallet** +- Real multi-network balances — Bitcoin, Ethereum (Sepolia testnet), + Arbitrum and Polygon (real mainnet) — with real USD totals and per-asset + fiat values (CoinGecko) +- Multi-account support: switching between accounts on one wallet, with + bounded automatic discovery of previously-used accounts after a fresh + import (see `docs/ARCHITECTURE.md` for the real, stated limits of this — + it's not unlimited, and can't discover a genuinely zero-activity + account) +- Receive: network/token picker, real QR code, copy/share +- Send: pick a token → amount (crypto/fiat toggle) → review (real fee + quoting) → confirm — **executes a real, live send**, covering both + native assets and ERC-20 tokens (USDT/USDT0) → success, with a working + link to a real block explorer for the actual transaction +- Activity: real transaction history via the WDK Indexer API, with + chain/token/type filters that only ever show combinations that actually + exist (no "Bitcoin + ETH" nonsense), grouped by Today/Yesterday/Earlier + +**Everywhere** +- A fully responsive UI system — every screen scales correctly across + phone and tablet + +**Not yet built**, so you don't assume otherwise: restoring a wallet *from* +a cloud backup (the download/decrypt side — upload works, download doesn't +have a screen yet), and Tron/GasFree support (on hold — see +`docs/WDK_INTEGRATION.md`). + +**A known, currently unresolved issue** worth reading about before you +assume it's something you broke: EVM balance fetches can begin timing out +after an extended session, across all EVM networks simultaneously, +regardless of RPC provider — traced directly to how WDK's own package +manages its underlying provider connections, not something fixable from +this app's code. See `docs/TROUBLESHOOTING.md`'s last entry. + +## Quick start ```bash -npm install -``` +# 1. Read docs/ENVIRONMENT.md first — Node/npm/JDK version mismatches +# cause confusing native build failures, not clean errors. -## 🔑 Environment Setup - -**Optional but Recommended:** Configure API keys for balance, transaction data, and Tron network support: +# 2. Install +npm install -```bash -# Copy the example environment file +# 3. Copy env template and fill in what you need (see docs/CLOUD_BACKUP.md +# if you want cloud backup working; the rest of the app works without it) cp .env.example .env -# Edit .env and configure the following: -# EXPO_PUBLIC_WDK_INDEXER_BASE_URL=https://wdk-api.tether.io -# EXPO_PUBLIC_WDK_INDEXER_API_KEY=your_wdk_api_key_here -# EXPO_PUBLIC_TRON_API_KEY=your_tron_api_key_here (optional, for Tron network) -# EXPO_PUBLIC_TRON_API_SECRET=your_tron_api_secret_here (optional, for Tron network) -``` - -**Note:** The WDK Indexer API key is used for balance and transaction API requests. While not mandatory for development, it enables full functionality. Tron API keys are optional and only needed if you want to use the Tron network. Get your free WDK Indexer API key in the [WDK docs](https://docs.wallet.tether.io/). - -## 🔧 Provider Configuration (Recommended) +# 4. Generate native projects +npx expo prebuild --clean -**For Better Performance:** The app uses public RPC endpoints by default, which may have rate limits and variable performance. For a better experience, customize provider URLs in `src/config/get-chains-config.ts`: - -### Customizable Endpoints - -Edit `src/config/get-chains-config.ts` to update these provider URLs: - -**Ethereum** -```typescript -ethereum: { - provider: 'https://eth.merkle.io', // Replace with your Ethereum RPC URL - bundlerUrl: 'https://api.candide.dev/public/v3/ethereum', - paymasterUrl: 'https://api.candide.dev/public/v3/ethereum', -} -``` - -**Arbitrum** -```typescript -arbitrum: { - provider: 'https://arb1.arbitrum.io/rpc', // Replace with your Arbitrum RPC URL - bundlerUrl: 'https://api.candide.dev/public/v3/arbitrum', - paymasterUrl: 'https://api.candide.dev/public/v3/arbitrum', -} -``` - -Do the same for other chains. - - -## 🚀 Run - -Then start the app: - -```bash -# iOS simulator -npm run ios - -# Android emulator/device +# 5. Run +npm run ios # or npm run android ``` -## 📁 Project Structure - -``` -src/ -├── app/ # App screens (Expo Router file-based routing) -│ ├── _layout.tsx # Root layout with providers -│ ├── index.tsx # Entry point & routing logic -│ ├── onboarding/ # Onboarding flow screens -│ ├── wallet-setup/ # Wallet creation/import flows -│ ├── wallet.tsx # Main wallet dashboard -│ ├── assets.tsx # Asset list screen -│ ├── activity.tsx # Transaction history -│ ├── send/ # Send flow screens -│ ├── receive/ # Receive flow screens -│ ├── authorize.tsx # Biometric authentication -│ ├── settings.tsx # App settings -│ ├── scan-qr.tsx # QR code scanner -│ └── token-details.tsx # Individual token details -├── components/ # Reusable UI components -│ ├── onboarding/ # Onboarding components -│ ├── ui/ # Base UI components -│ └── *.tsx # Shared components -├── config/ # Configuration files -│ ├── assets.ts # Token/asset configurations -│ ├── avatar-options.ts # Wallet avatar configurations -│ ├── networks.ts # Network configurations -│ └── get-chains-config.ts # Chain-specific settings & provider URLs -├── services/ # Business logic & external services -│ └── pricing-service.ts # Fiat pricing via Bitfinex -├── hooks/ # Custom React hooks -│ ├── use-debounced-navigation.ts # Debounced navigation to prevent rapid taps -│ ├── use-keyboard.ts # Keyboard visibility detection -│ └── use-wallet-avatar.ts # Wallet avatar management -└── utils/ # Utility functions - ├── gas-fee-calculator.ts # Gas fee estimation & network utilities - ├── format-amount.ts # Amount formatting helpers - ├── format-token-amount.ts # Token-specific amount formatting - ├── format-usd-value.ts # USD value formatting - ├── get-display-symbol.ts # Token symbol display utilities - ├── get-denomination-value.ts # Token denomination utilities - ├── parse-worklet-error.ts # Worklet error parsing for better UX - └── recent-tokens.ts # Recent token tracking -``` - -## 🏗️ Architecture & Key Flows - -### App Architecture -The app follows a clean architecture pattern with clear separation of concerns: - -1. **Providers Layer** (`_layout.tsx`) - - `WalletProvider`: Manages wallet state, blockchain interactions, and WDK service - - `ThemeProvider`: Handles dark mode and custom theming - - `NavigationThemeProvider`: React Navigation theme configuration - -2. **Screen Layer** (`app/` directory) - - File-based routing via Expo Router - - Each screen is a self-contained React component - - Navigation handled automatically based on file structure - -3. **Business Logic** (`services/` directory) - - Pricing service for real-time fiat conversion - -4. **Configuration** (`config/` directory) - - Centralized network and asset configurations - - Chain-specific RPC endpoints and settings - -### User Flows - -#### First-Time User (Create Wallet) -1. **Onboarding** → View welcome screen with app features -2. **Wallet Setup** → Choose "Create New Wallet" -3. **Name Wallet** → Set a custom wallet name -4. **Secure Wallet** → View and backup 12-word seed phrase -5. **Confirm Phrase** → Verify seed phrase knowledge -6. **Complete** → Wallet created and unlocked -7. **Wallet Dashboard** → Access main wallet interface - -#### Returning User (Import Wallet) -1. **Onboarding** → View welcome screen -2. **Wallet Setup** → Choose "Import Existing Wallet" -3. **Import Wallet** → Enter 12-word seed phrase -4. **Name Wallet** → Set a custom wallet name -5. **Complete** → Wallet imported and unlocked -6. **Wallet Dashboard** → Access main wallet interface - -#### Existing User (App Launch) -1. **Entry Point** (`index.tsx`) → Check wallet status -2. **Authorization** → Biometric authentication (Face ID/Touch ID) -3. **Wallet Dashboard** → Access wallet after unlock - -#### Send Flow -1. **Wallet Dashboard** → Tap "Send" button -2. **Select Token** → Choose token to send (BTC, USD₮, XAU₮) -3. **Select Network** → Choose blockchain network -4. **Send Details** → Enter recipient address, amount, and review fees -5. **Confirm** → Approve transaction -6. **Transaction Submitted** → View transaction status - -#### Receive Flow -1. **Wallet Dashboard** → Tap "Receive" button -2. **Select Token** → Choose token to receive -3. **Select Network** → Choose blockchain network -4. **Receive Details** → View QR code and address, copy or share - -## 🌐 Supported Networks & Operations - -This starter supports the following blockchain networks and operations: - -| Network | Balance | History | Send | Receive | Gas Sponsorship | -|---------|---------|---------|------|---------|-----------------| -| **Bitcoin (SegWit)** | ✅ | ✅ | ✅ | ✅ | N/A | -| **Lightning** | ✅ | ✅ | ✅ | ✅ | N/A | -| **Ethereum** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Polygon** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Arbitrum** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **TON** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Tron** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Solana** | ✅ | ✅ | ✅ | ✅ | ✅ | - -### Token Support - -| Token | Symbol | Networks | -|-------|--------|----------| -| **Bitcoin** | BTC | Bitcoin (SegWit), Lightning | -| **Tether USD** | USD₮ | Ethereum, Polygon, Arbitrum, TON, Tron, Solana | -| **Tether Gold** | XAU₮ | Ethereum | - -### Key Features -- **Gas Sponsorship**: EVM networks (Ethereum, Polygon, Arbitrum) and other supported chains offer gasless transactions via paymasters -- **Multi-Network**: Send the same token across different networks based on preference and fees -- **Real-Time Data**: Live balance and transaction updates via WDK Indexer -- **QR Code Support**: Generate and scan QR codes for easy address sharing - -## 🔒 Security Features - -This starter implements multiple layers of security for protecting user assets: - -### Secure Key Management -- **BareKit Worklets**: Cryptographic operations run in isolated worklet context -- **Secret Manager**: Keys stored encrypted using native secure storage (iOS Keychain, Android KeyStore) -- **No Key Exposure**: Private keys never leave the secure context or device -- **BIP39 Compliant**: Standard 12-word mnemonic seed phrase generation - -### Authentication -- **Biometric Lock**: Face ID/Touch ID for wallet unlock -- **App Lock**: Wallet locked on app background/close -- **Session Management**: Secure session handling with automatic timeout - -### Transaction Security -- **User Confirmation**: All transactions require explicit user approval -- **Amount Verification**: Clear display of amounts and fees before signing -- **Address Validation**: Input validation for recipient addresses -- **Network Selection**: User must explicitly choose network to prevent errors - -### Best Practices -- **No Analytics**: No user data or transaction info sent to third parties -- **Local Storage**: All wallet data stored locally on device -- **Open Source**: Fully auditable codebase -- **Non-Custodial**: Users have complete control of their private keys - -## ⚙️ Polyfills & Build Configuration - -### Node.js Polyfills -The app includes comprehensive Node.js polyfills for React Native compatibility via `@tetherto/wdk-react-native-provider/metro-polyfills` -### Native Modules -- **Sodium**: `sodium-javascript` (WebAssembly-based cryptography) -- **Random**: `react-native-get-random-values` (secure randomness) -- **PBKDF2**: `react-native-fast-pbkdf2` (key derivation) -- **TCP**: `react-native-tcp-socket` (for Bitcoin Electrum) - -## 🧪 Available Scripts - -| Script | Description | -|--------|-------------| -| `npm start` | Start Expo development server with dev client | -| `npm run ios` | Run on iOS simulator | -| `npm run android` | Run on Android emulator/device | -| `npm run web` | Run in web browser | -| `npm run prebuild` | Generate native project files | -| `npm run prebuild:clean` | Clean and regenerate native project files | -| `npm run lint` | Run ESLint to check code quality | -| `npm run lint:fix` | Run ESLint and auto-fix issues | -| `npm run format` | Format code with Prettier | -| `npm run format:check` | Check code formatting without making changes | -| `npm run typecheck` | Run TypeScript type checking | - -## 🔗 Version & Compatibility - -### Core Dependencies -- **Expo**: ~54.0.8 -- **React**: 19.1.0 -- **React Native**: 0.81.4 -- **TypeScript**: ~5.9.2 - -### Key Features -- **New Architecture**: Enabled in `app.json` for improved performance -- **React Native Reanimated**: ~4.1.0 for smooth animations -- **React Compiler**: Enabled for automatic memoization - -### Platform Requirements -- **Android**: minSdkVersion 29, compileSdkVersion 36 -- **iOS**: Latest Xcode toolchain recommended -- **Node.js**: 22+ required - -### WDK Packages -- `@tetherto/wdk-react-native-provider`: Main wallet provider -- `@tetherto/wdk-uikit-react-native`: UI components library -- `@tetherto/wdk-pricing-provider`: Fiat pricing integration -- `@tetherto/wdk-pricing-bitfinex-http`: Bitfinex price data provider -- `@tetherto/pear-wrk-wdk`: BareKit worklets runtime - -## 🎨 Customization Guide - -### Adding New Tokens -1. Add token configuration in `src/config/assets.ts`: -```typescript -export const assetConfig: Record = { - newtoken: { - name: 'New Token', - icon: require('../../assets/images/tokens/newtoken-logo.png'), - color: '#YOUR_COLOR', - supportedNetworks: [NetworkType.ETHEREUM], - }, -}; -``` - -2. Add token icon to `assets/images/tokens/` - -### Adding New Networks -1. Add network configuration in `src/config/networks.ts`: -```typescript -[NetworkType.NEW_NETWORK]: { - id: 'new-network', - name: 'New Network', - gasLevel: 'Low', - gasColor: '#34C759', - icon: require('../../assets/images/chains/new-network-logo.png'), - color: '#YOUR_COLOR', -} -``` - -2. Add chain configuration in `src/config/get-chains-config.ts`: -```typescript -newnetwork: { - chainId: YOUR_CHAIN_ID, - blockchain: 'newnetwork', - provider: 'https://your-rpc-url.com', - // Add other chain-specific configuration -} -``` - -3. Add chain logo to `assets/images/chains/` - -### Customizing Theme & Brand -Update the brand configuration in `src/app/_layout.tsx`: -```typescript - -``` - -## 🐛 Troubleshooting - -### Common Issues - -**Metro bundler cache issues** -```bash -npx expo start -c -``` - -**Native build issues** -```bash -npm run prebuild:clean -cd ios && pod install -cd android && ./gradlew clean -``` - -**Type errors after updates** -```bash -npm run typecheck -``` - -### Development Tips -- Use Expo Dev Client for faster development cycles -- Enable Fast Refresh for instant UI updates -- Check Metro bundler logs for build issues -- Use React DevTools for component debugging -- Monitor network requests in browser DevTools (web) or Reactotron (native) +## Tech stack -## 📜 License +| Layer | Choice | +|---|---| +| Framework | Expo SDK 55 (managed, with `expo prebuild`), React Native 0.83 | +| Navigation | Expo Router (file-based) | +| Wallet SDK | `@tetherto/wdk-react-native-core` + `@tetherto/wdk` (Bare-runtime worklet architecture — see [`ARCHITECTURE.md`](docs/ARCHITECTURE.md)) | +| Data layer | TanStack Query — WDK hooks called directly from screens (an earlier mock/WDK DI seam exists in `src/data/` but is no longer used by any screen; see `docs/ARCHITECTURE.md`) | +| State | Zustand (small, single-purpose stores — session, password session, lock suppression) | +| Encryption | `@tetherto/wdk-utils` (AES-256-GCM + scrypt) | +| Secure storage | `expo-secure-store` (iOS Keychain / Android Keystore) | +| Cloud backup | `@tetherto/wdk-backup-cloud` — CloudKit (via WebView) + Google Drive | +| Language | TypeScript, strict mode | -This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE) file for details. +## Project name and identifiers -## 🤝 Contributing +- Bundle ID / package name: `io.tether.wdkstarterreactnative` +- If you fork this for your own app, update `app.json`'s `ios.bundleIdentifier` + and `android.package` — several native setup steps (Google OAuth clients, + Apple's CloudKit container) are tied to these identifiers. -Contributions are welcome! Please feel free to submit a Pull Request. +## License -- Read the [code of conduct](CODE_OF_CONDUCT.md) -- See [contributing guide](CONTRIBUTING.md) +Apache-2.0 — see [`LICENSE`](LICENSE). -## 🆘 Support +## Contributing -For support, please: -- Check the [WDK documentation](https://docs.wallet.tether.io) -- Open an issue on the GitHub repository -- Join the WDK developer community +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the branch strategy and coding +conventions used in this repo. diff --git a/app.json b/app.json index c2f76ee..6c3ff61 100644 --- a/app.json +++ b/app.json @@ -4,13 +4,19 @@ "slug": "wdk-starter-react-native", "version": "1.0.0", "orientation": "portrait", - "developmentClient": false, "icon": "./assets/images/icon.png", "scheme": "wdkstarterreactnative", - "newArchEnabled": true, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.anonymous.wdkstarterreactnative", + "bundleIdentifier": "io.tether.wdkstarterreactnative", + "entitlements": { + "com.apple.developer.icloud-services": [ + "CloudKit" + ], + "com.apple.developer.icloud-container-identifiers": [ + "iCloud.io.tether.wdkshowcase" + ] + }, "infoPlist": { "UIStatusBarStyle": "UIStatusBarStyleLightContent", "UIViewControllerBasedStatusBarAppearance": false, @@ -27,7 +33,7 @@ }, "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false, - "package": "com.anonymous.wdkstarterreactnative", + "package": "io.tether.wdkstarterreactnative", "statusBar": { "style": "light", "backgroundColor": "#000000" @@ -48,6 +54,13 @@ "userInterfaceStyle": "light", "plugins": [ "expo-router", + [ + "expo-navigation-bar", + { + "visibility": "hidden", + "behavior": "overlay-swipe" + } + ], [ "expo-splash-screen", { @@ -57,12 +70,6 @@ "backgroundColor": "#FF6501" } ], - [ - "expo-camera", - { - "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera" - } - ], [ "expo-build-properties", { @@ -75,11 +82,18 @@ } } ], - "./plugins/withAndroidSubprojects.js" - ], - "experiments": { - "typedRoutes": true, - "reactCompiler": true - } + "./modules/cloud-backup/withModularHeaders.js", + "expo-secure-store", + [ + "@react-native-google-signin/google-signin", + { "iosUrlScheme": "com.googleusercontent.apps.518914426511-c46v1j9olhu5trra830bfo13cdj3ge4h" } + ], + [ + "expo-secure-store", + { + "configureAndroidBackup": true + } + ] + ] } } diff --git a/assets/docs/demo-thumbnail.png b/assets/docs/demo-thumbnail.png deleted file mode 100644 index fded7a1..0000000 Binary files a/assets/docs/demo-thumbnail.png and /dev/null differ diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png new file mode 100644 index 0000000..537235a Binary files /dev/null and b/assets/images/adaptive-icon.png differ diff --git a/assets/images/android-icon-background.png b/assets/images/android-icon-background.png index 5a9cd1b..f98bca7 100644 Binary files a/assets/images/android-icon-background.png and b/assets/images/android-icon-background.png differ diff --git a/assets/images/android-icon-foreground.png b/assets/images/android-icon-foreground.png index 096ce9c..d74d2dc 100644 Binary files a/assets/images/android-icon-foreground.png and b/assets/images/android-icon-foreground.png differ diff --git a/assets/images/android-icon-monochrome.png b/assets/images/android-icon-monochrome.png index a833c27..1c7b523 100644 Binary files a/assets/images/android-icon-monochrome.png and b/assets/images/android-icon-monochrome.png differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png index 712ea82..1560632 100644 Binary files a/assets/images/favicon.png and b/assets/images/favicon.png differ diff --git a/assets/images/icon.png b/assets/images/icon.png index 6324f44..d32556f 100644 Binary files a/assets/images/icon.png and b/assets/images/icon.png differ diff --git a/assets/images/splash.png b/assets/images/splash.png new file mode 100644 index 0000000..0e89705 Binary files /dev/null and b/assets/images/splash.png differ diff --git a/assets/images/wdk-logo.png b/assets/images/wdk-logo.png index 6c60a72..4ebc764 100644 Binary files a/assets/images/wdk-logo.png and b/assets/images/wdk-logo.png differ diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..636bcd9 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,9 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + ['module-resolver', { root: ['./'], alias: { '@': './src' } }], + ], + }; +}; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..89ea246 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,204 @@ +# Architecture + +This document explains how the app is put together and, more importantly, +**why** — several structural decisions here exist specifically to avoid +problems this project hit during development. If you're going to deviate +from a pattern described here, read the reasoning first. + +## The big picture (as it actually is today) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Screens (src/app/**) │ +│ import DIRECTLY from '@/wdk/hooks/useWalletData', │ +│ '@/wdk/useSend', '@/wdk/indexer', '@/state/*' — see the │ +│ note below on why this differs from an earlier design │ +└───────────────────────┬───────────────────────────────────────┘ + │ +┌───────────────────────▼───────────────────────────────────────┐ +│ WDK integration layer (src/wdk/**) │ +│ hooks/useWalletData.ts — balances, accounts, transactions │ +│ useSend.ts — real sends (native + ERC-20 token transfers) │ +│ indexer.ts — WDK Indexer API (Activity tab) │ +│ pricing.ts — CoinGecko (USD totals/fiat values) │ +└───────────────────────┬───────────────────────────────────────────┘ + │ +┌───────────────────────▼───────────────────────────────────────────┐ +│ WDK itself (Bare-runtime worklet) │ +│ @tetherto/wdk-react-native-core + @tetherto/wdk │ +└───────────────────────────────────────────────────────────────────────┘ +``` + +## A real correction: the "data seam" described in an earlier version of +## this doc is no longer how the app works — this isn't a stale detail, +## it's a structural drift worth understanding + +An earlier phase of this project built a `src/data/` layer (`USE_WDK` flag, +`RepositoriesProvider`, `walletQueries.ts`) specifically so screens could be +built and tested against mock data before any real WDK integration existed, +without needing to touch screen code later to swap mocks for the real +thing. + +**That layer still exists as files on disk, but it is dead code.** Checked +directly rather than assumed: zero screens under `src/app/` import from +`@/data` anymore; all of them (14 files, as of this writing) import +directly from `@/wdk/hooks/useWalletData` and friends. As the WDK +integration deepened — multi-network EVM support, real sends across native +and token assets, multi-account discovery, real transaction history via the +Indexer API — building each new piece directly against WDK's own hooks was +more direct than continuing to thread everything through an intermediate +seam that no longer had a second implementation (mock) actively +maintained behind it. The seam's original purpose (build UI without a +funded wallet) was genuinely useful during onboarding, but nothing since +has gone back through it. + +**Recommendation, not yet acted on:** `src/data/` should be removed before +this PR merges, or in an immediate follow-up. Shipping it as-is means +carrying real, unused, misleading-if-read code — a reviewer or future +contributor finding `RepositoriesProvider` and `USE_WDK` would reasonably +assume it's load-bearing, when it isn't. This paragraph exists so that +assumption doesn't get made silently; the actual call on whether to delete +it now or track it as a follow-up ticket is a judgment call for whoever's +merging this. + +## Why WDK integration is hook-based, not a repository + +WDK's actual API (`useWalletManager`, `useAccount`, `useBalancesForWallet`, +`useWdkApp`) is a set of **React hooks**, not `Promise`-returning service +methods. An earlier design attempted to wrap WDK behind a plain repository +interface (matching the mock's shape exactly) — this fights WDK's own +design, since hooks can only be called from React components/hooks, not +arbitrary async functions. + +The current design (`src/wdk/hooks/`) works *with* WDK's grain: hooks that +internally call WDK's hooks and reshape their output into the domain +models screens consume. + +**A related, real constraint this caused, worth knowing before touching +`useSend.ts` or `wdk/indexer.ts`:** anywhere real, imperative logic is +needed outside a component's render (executing a send, batch-checking +several account indices for the multi-account discovery feature), WDK's +hook-only surface means that logic has to either (a) be structured as a +component that calls the hook and reports results via a callback (see +`AccountDiscoveryProbe.tsx`), or (b) resolve the account via a normal +hook call at the call site and pass the resolved object into a plain +function (see `useSend.ts`'s `sendAsset()`). Neither is a repository +pattern — both are deliberate accommodations to WDK's hook-based API, not +inconsistency. + +## The WDK integration itself + +See [`WDK_INTEGRATION.md`](WDK_INTEGRATION.md) for the full step-by-step +account, including exact package choices, the multi-network/multi-account/ +transaction-history integrations, and the real, non-obvious bugs found and +fixed along the way. + +## Wallet identity + +`DEFAULT_WALLET_ID = 'primary'` (`src/wdk/walletIdentity.ts`) is a +**fixed constant** — every install of this app uses the same wallet ID. +This is a deliberate simplification for a single-wallet reference app, not +an oversight. + +**Known consequence, not a bug:** because the ID is fixed, testing this app +repeatedly under the same Apple ID/Google account (reinstalling, resetting +simulators) will reuse the same cloud-backup storage slot each time — a new +backup overwrites the previous one. See `docs/CLOUD_BACKUP.md`. + +## Multi-account architecture — a deliberate, real trade-off + +This app supports switching between multiple accounts (BIP44-style +derivation indices) on the same wallet, but **which accounts exist, their +names, and which one is active are held entirely in memory** — nothing is +persisted to disk (`src/state/accounts.ts`). This wasn't the original +design; it replaced a version that persisted this via `expo-secure-store`, +after a real, confirmed bug: on iOS, Keychain-backed storage survives full +app deletion (documented Apple/Expo behavior), so deleting the app and +importing a *different* wallet still showed the *previous* wallet's +account list. Given the explicit direction to store only genuinely +sensitive data in Keychain and fetch everything else live, the account list +was made purely in-memory instead. + +**The real trade-off this creates:** every fresh app launch starts with +just "Account 1." To make previously-used accounts reappear automatically +without persisting anything, `AccountDiscovery.tsx` checks a bounded range +of indices (currently 1 through 5, sequentially, with retries on failed +checks) for genuine on-chain activity on unlock, and surfaces any that have +real balances. This has a hard, structural limit: **an account with zero +balance and zero transaction history cannot be discovered this way** — +there is no signal to detect, since a seed phrase's derivation path carries +no information about which indices were ever actually used. The only way +to recover a genuinely zero-activity account's existence across a fresh +install would be storing that metadata somewhere that *does* survive +deletion — e.g., alongside the encrypted mnemonic in a cloud backup — which +is not currently implemented. + +See `WDK_INTEGRATION.md` for the two real bugs found while building +discovery itself (a wrong readiness signal, and a stale-cache timing issue +in WDK's own balance hook) — both fixed, but worth reading if you touch +`AccountDiscovery.tsx`/`AccountDiscoveryProbe.tsx` again. + +## Session / lock architecture + +See [`SECURITY.md`](SECURITY.md) for the full password-vault and +lock/unlock design. The short version: three small, single-purpose Zustand +stores (`state/session.ts`, `state/passwordSession.ts`, +`state/lockSuppression.ts`) plus two always-mounted root components +(`AutoLockOnBackground`, `WdkSessionGate`) — deliberately kept separate +rather than one large auth context, since each piece independently caused a +real, distinct bug during development that was easier to isolate and fix +because they were separate. + +**One more always-mounted root component since that was written:** +`AccountDiscovery` (see above) — mounted alongside the others, reacting to +`useWdkSession()`'s status specifically (not `useWalletManager().status` +directly — the two are genuinely different signals; see +`WDK_INTEGRATION.md` for why that distinction is load-bearing here). + +## Responsive design system + +Every screen scales with device size via `src/theme/responsive.ts` +(`useResponsive()` — `moderateScale`, `wp`, `hp`), anchored to the design +prototype's own canvas size (380×720 — not an arbitrary number, it's +literally the prototype HTML's own dimensions). Applied consistently +through `Text`, `Button`, `ScreenHeader`, `TextField`, and every screen +built since — new components should follow the same pattern rather than +hardcoding pixel values. + +**A specific, recurring gotcha worth flagging explicitly:** small/bold text +at a fixed `fontSize` without an explicit `lineHeight` renders with visibly +clipped ascenders/descenders in this RN/Yoga setup, especially noticeable +at small sizes (this hit the Send Review screen's large amount text, and +Activity's tiny chain-badge letter, independently — same root cause both +times). Always pair an explicit `fontSize` with a deliberately generous +`lineHeight` (roughly 1.3–1.4x), not just the default. + +## Shared icon rendering — `AssetIcon` + +`src/components/AssetIcon.tsx` is the single source of truth for +token/chain icon rendering (Home, Receive, Send, Review all use it) — +consolidated after Home and Receive each independently built near-identical +icon logic. It handles two genuinely different real asset-logo styles +(some official brand assets are full "coin" icons with a color already +baked in — Bitcoin, Tether's USDT mark; others are just the mark on a +transparent background — Ethereum, Arbitrum, Polygon — needing a colored +circle behind them), and proportionally scales its chain-badge overlay to +whatever `size` is passed in, rather than a fixed pixel value that only +looked right at one specific size. + +## Cloud backup architecture + +See [`CLOUD_BACKUP.md`](CLOUD_BACKUP.md). One decision worth calling out +here: the person can choose **either** iCloud or Google Drive on **either** +platform — this required removing a `Platform.OS`-based gate that a +reference implementation this was adapted from used, and required +understanding that neither provider is actually platform-locked at a +technical level (CloudKit auth here is a WebView, not a native-iOS-only +API; Google Sign-In is genuinely cross-platform). + +**Not yet built:** restore-from-cloud (download + decrypt to recover a +wallet on a fresh install). `CloudBackupContext.downloadBackup()` exists +and is ready; no screen calls it yet. This matters more than it might +sound like, given the multi-account trade-off above — a cloud restore +flow would be the natural place to also carry "how many accounts existed" +metadata that a plain seed-phrase re-import structurally cannot. diff --git a/docs/CLOUD_BACKUP.md b/docs/CLOUD_BACKUP.md new file mode 100644 index 0000000..09239ce --- /dev/null +++ b/docs/CLOUD_BACKUP.md @@ -0,0 +1,423 @@ +# Cloud Backup + +Lets a person back up an encrypted copy of their recovery phrase to a +cloud provider, and (once the restore screen is built — see the note at +the bottom) recover it on another device. + +This guide is adapted from the WDK showcase app's own cloud-backup setup +guide, with the differences called out explicitly wherever this app does +something different. If you've set up cloud backup for the showcase +before, read the **"How this app differs from the showcase"** section +first — several steps are genuinely different here, not just renamed. + +--- + +## How this app differs from the showcase + +The showcase hard-gates by platform: iOS gets CloudKit only, Android gets +Google Drive only, and the person never chooses. **This app lets the +person choose either provider on either platform.** Neither provider is +actually platform-locked at a technical level: + +- CloudKit access here goes through a `WebView` running CloudKit JS with a + **web-based** Apple ID sign-in — not a native-iOS-only API. It works + identically on Android. +- Google Sign-In (`@react-native-google-signin/google-signin`) is + genuinely cross-platform. + +Two concrete consequences of this choice, both of which are **extra setup +steps the showcase's guide doesn't have**, because it never needed them: + +1. **Google Sign-In needs an iOS OAuth client too** (Section 3 below) — + the showcase only ever runs Google Sign-In on Android. +2. **The encryption passphrase is the person's own app password, not a + fixed one.** The showcase encrypts every backup with a single + `.env`-configured passphrase shared by every install of the app — + explicitly documented in that codebase as a demo-only shortcut, not + something to do in production. This app instead recovers the person's + real app password (`passwordVault.getAppPassword()`, see + `SECURITY.md`) and encrypts with that. There is **no** + `EXPO_PUBLIC_CLOUD_BACKUP_PASSPHRASE` variable in this app — if you're + used to the showcase, don't go looking for it. + +One more small difference: the CloudKit callback URL is an `.env` variable +here (`EXPO_PUBLIC_CLOUDKIT_CALLBACK_URL`), not a hardcoded JS constant +like the showcase — so forking this starter for your own app doesn't +require editing source code for this, just your `.env`. + +--- + +## Security model + +``` +Wallet mnemonic (WDK secure storage) + ↓ +getSeedAndEntropyFromMnemonic() WDK's own crypto: { encryptionKey, encryptedEntropyBuffer } + ↓ +JSON.stringify(...) combine into one payload string + ↓ +encryptPayload(payload, appPassword) AES-256-GCM + scrypt, keyed to the PERSON'S password + ↓ +JSON.stringify(encryptedPayload) serialize for cloud storage + ↓ +uploadBackup(payload, walletId) stored under a wallet-specific record/file +``` + +The cloud only ever stores encrypted data. Restoring requires **all** of: +the encrypted backup, the person's app password, and the WDK worklet +(for `getMnemonicFromEntropy`). Without all three, the backup cannot be +decrypted — including by anyone who only has a copy of this app's source +code, since the passphrase isn't baked into the app. + +**Per-wallet storage:** each wallet's backup is stored under a name derived +from its wallet ID: +- iOS CloudKit: `recordName = "wallet_"` +- Google Drive: `fileName = "wallet_.json"` + +This app uses a **fixed** wallet ID (`'primary'` — see `ARCHITECTURE.md`), +so every install computes the same record name. Testing repeatedly under +your own Apple ID/Google account will overwrite the same backup slot each +time — expected, not a bug. + +--- + +## Required `.env` variables + +```bash +# iOS — CloudKit +EXPO_PUBLIC_CLOUDKIT_CONTAINER_ID=iCloud.io.tether.wdkstarterreactnative +EXPO_PUBLIC_CLOUDKIT_API_TOKEN= +EXPO_PUBLIC_CLOUDKIT_CALLBACK_URL= + +# Google Drive — BOTH platforms +EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID= +EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID= +``` + +The app runs fine with none of these set — you just can't test cloud +backup until they're filled in. There is deliberately **no** payload +passphrase variable — see above. + +--- + +## 1. iOS Cloud Backup Setup (Apple CloudKit) + +### Step 1 — Apple Developer Portal: create the App ID and iCloud container + +1. Go to [developer.apple.com](https://developer.apple.com) → + **Certificates, Identifiers & Profiles** +2. **Identifiers** → **+** +3. **App IDs** → **App** → Continue +4. Description: `WDK Starter Wallet`, Bundle ID: `io.tether.wdkstarterreactnative` + (must exactly match `expo.ios.bundleIdentifier` in `app.json` — if + you've forked this app under your own bundle ID, use that instead) +5. Under **Capabilities**, check **iCloud** → **Edit** +6. Check **CloudKit** +7. Under **Containers**, click **+**, enter: + `iCloud.io.tether.wdkstarterreactnative` +8. **Continue** → **Register** + +### Step 2 — CloudKit Console: create the schema + +1. Go to [icloud.developer.apple.com](https://icloud.developer.apple.com) +2. **CloudKit Database** +3. Select your container from the top dropdown +4. Left sidebar → **Schema** → **Record Types** → **+** +5. Record Type name: `WalletBackup` +6. Add three fields: + +| Field Name | Type | +|---|---| +| `encryptionKey` | String | +| `savedAt` | String | +| `cloudEmail` | String | + +7. **Save** +8. Left sidebar, bottom → **Deploy Schema Changes...** → confirm + +> ⚠️ **Saving the schema is not enough — it must be deployed.** The Deploy +> step is what makes it active in the Development environment. Skipping +> this produces confusing schema errors on your very first upload attempt. + +### Step 3 — CloudKit Console: generate the API token + +1. Still in CloudKit Dashboard: left sidebar → **Settings** → + **Tokens & Keys** +2. **Generate New Token** (or **+**) +3. Fill in: + - **Name:** `WDK Starter Wallet` + - **Sign in Callback:** select **URL Redirect** + - **Redirect URL:** whatever you set `EXPO_PUBLIC_CLOUDKIT_CALLBACK_URL` + to (see below) — it does **not** need to be a real, reachable page; + this app intercepts the redirect before it ever actually loads, it + just needs to match character-for-character. + - **Allowed Origins:** Any Domain + - **Discoverability:** check this if you want the person's real name + (not email — Apple never exposes email via CloudKit JS) visible when + they sign in. Optional. +4. **Save** +5. **Copy the token immediately** — some CloudKit Dashboard versions only + show it once. + +> ⚠️ **Sign in Callback must be URL Redirect, not Post Message.** Post +> Message mode is for browser popups and does not work with this app's +> WebView-based sign-in. If you accidentally create a Post Message token, +> just generate a new one — there's no way to convert an existing one. + +### Step 4 — add to `.env` + +```bash +EXPO_PUBLIC_CLOUDKIT_CONTAINER_ID=iCloud.io.tether.wdkstarterreactnative +EXPO_PUBLIC_CLOUDKIT_API_TOKEN=your_token_here +EXPO_PUBLIC_CLOUDKIT_CALLBACK_URL=https://your-chosen-domain.example/cloudkit-callback +``` + +### Step 5 — rebuild + +```bash +npx expo prebuild --clean +npx expo run:ios --device +``` + +--- + +## 2. Google Drive Setup — Android side + +### Step 1 — get your debug SHA-1 fingerprint + +Run this after your first `expo prebuild`: + +```bash +cd android && ./gradlew signingReport +``` + +Look for `Variant: debug` and copy the `SHA1` line: +``` +SHA1: A1:B2:C3:D4:E5:F6:G7:H8:I9:J0:K1:L2:M3:N4:O5:P6:Q7:R8:S9:T0 +``` + +Or directly from the debug keystore, without needing the `android/` folder: +```bash +keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android +``` + +**Remember:** this is your *debug* SHA-1 only. Before shipping a real +build, you'll also need to register the SHA-1 your release signing (e.g. +EAS Build) uses — get it via `eas credentials`. Registering only the +debug SHA-1 means sign-in works locally but fails for anyone testing a +release build. + +### Step 2 — Google Cloud Console: create the Android OAuth client + +1. Go to [console.cloud.google.com](https://console.cloud.google.com) → + **New Project** → name it → Create +2. **APIs & Services → Credentials → + Create Credentials → OAuth Client ID** +3. Application type: **Android** +4. Package name: `io.tether.wdkstarterreactnative` (must exactly match + `expo.android.package` in `app.json`) +5. SHA-1: paste from Step 1 +6. **Create** + +This client is required (it registers your SHA-1 with Google) even though +you never reference its ID directly in code. + +### Step 3 — create the Web OAuth client + +This is the one that actually goes in your `.env`. + +1. **+ Create Credentials → OAuth Client ID** +2. Application type: **Web application** +3. Name: anything recognizable +4. **Create** +5. Copy the **Client ID** — looks like `123456789-abc.apps.googleusercontent.com` + +**Important:** this Web Client ID is scoped to the Google Cloud **project**, +not to a specific app — it can be reused across multiple apps in the same +project without conflict (e.g. if you also maintain the showcase app under +the same project). + +### Step 4 — enable the Drive API + +1. **APIs & Services → Library** +2. Search `Google Drive API` → **Enable** + +### Step 5 — configure the OAuth consent screen + +1. **APIs & Services → OAuth consent screen** +2. User type: **External** → **Create** +3. App name, support email → **Save and Continue** +4. **Scopes** → **Add or Remove Scopes** → search `drive.appdata`, check + it → **Update → Save and Continue** +5. **Test users** → **+ Add Users** → add the Gmail address(es) you'll + test with → **Save and Continue** + +> ⚠️ **The `drive.appdata` scope must be on the consent screen before +> sign-in.** Without it, Google silently returns an access token that +> lacks Drive permission — every backup attempt then fails with a 401, +> with no indication the scope is the problem. + +--- + +## 3. Google Drive Setup — iOS side (this app only, not the showcase) + +This entire section is **new setup the showcase's guide doesn't cover**, +because it only ever runs Google Sign-In on Android. Since this app lets +the person choose Google Drive on iOS too, Google Sign-In's iOS-native +requirements now apply. + +### Step 1 — create the iOS OAuth client + +Back in Google Cloud Console, same project as above: + +1. **+ Create Credentials → OAuth Client ID** +2. Application type: **iOS** +3. Bundle ID: `io.tether.wdkstarterreactnative` (your iOS bundle + identifier — note this is a **bundle ID**, not a SHA-1; iOS clients + work differently from Android ones) +4. **Create** +5. Copy the **Client ID** + +### Step 2 — add to `.env` + +```bash +EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=123456789-abc.apps.googleusercontent.com +EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=987654321-xyz.apps.googleusercontent.com +``` + +### Step 3 — register the URL scheme in `app.json` + +iOS needs a URL scheme registered so the OAuth redirect can route back +into the app — without it, sign-in fails **silently**, with no specific +error, which is what makes this step easy to skip and hard to diagnose +after the fact. + +The scheme is your iOS client ID, **reversed**: + +``` +123456789-xyz.apps.googleusercontent.com + ↓ becomes ↓ +com.googleusercontent.apps.123456789-xyz +``` + +Add to `app.json`'s `plugins` array: + +```json +[ + "@react-native-google-signin/google-signin", + { "iosUrlScheme": "com.googleusercontent.apps.123456789-xyz" } +] +``` + +This is the **non-Firebase** variant of the plugin (we pass client IDs +directly via `.env`, not a `GoogleService-Info.plist`), which is why it +needs this explicit `iosUrlScheme` option rather than being registered +with no arguments. + +### Step 4 — rebuild + +```bash +npx expo prebuild --clean +npx expo run:ios --device +``` + +A JS-only reload will **not** pick up this change — both the `.env` +values (inlined at bundle time) and the `app.json` plugin (native config) +require a real rebuild. + +--- + +## 4. Rebuild for Android + +```bash +npx expo prebuild --clean +npx expo run:android +``` + +### Android runtime notes + +- **`DEVELOPER_ERROR (10)`:** almost always means the SHA-1 for your + current build isn't registered under the Android OAuth client. Re-run + `./gradlew signingReport` and compare against what's registered in + Google Cloud Console. +- **Package name mismatch:** the Android client's package name must + exactly match `applicationId` in `android/app/build.gradle`, including + any build-variant suffix (e.g. `.debug`). +- **Drive scope prompt:** on first sign-in, Google shows a permission + dialog asking to grant Drive appdata access. If the person denies it, + every subsequent backup attempt fails until they re-authenticate and + accept. + +--- + +## Sharing a CloudKit container across multiple apps? + +Apple technically supports adding an existing container to a second app's +entitlements instead of creating a new one. Given this app's fixed wallet +ID (above), sharing a container with another app increases — low +probability, but real — collision risk if that app ever produces the same +record name. A separate container per app is the safer default; see +`ARCHITECTURE.md` for the full reasoning. + +--- + +## Troubleshooting + +### iOS + +| Error | Cause | Fix | +|---|---|---| +| `Native module not found` | Built with Expo Go instead of a real dev build | Run `npx expo run:ios --device` | +| `Crypto.getRandomValues must be defined` | Missing polyfill | See `docs/TROUBLESHOOTING.md` | +| `RNGoogleSignin: failed to determine clientID` | Missing `EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID`, or `.env` change not picked up | Add the var, then `npx expo start --clear` — a normal reload won't re-inline `.env` values | +| Google Sign-In fails silently, no useful error | Missing `iosUrlScheme` config plugin registration | See Section 3, Step 3 — needs `prebuild --clean` after adding | +| Token not received after Apple ID sign-in | API token has Post Message callback instead of URL Redirect | Regenerate the token with URL Redirect | +| Schema errors on upload | `WalletBackup` schema saved but not deployed | CloudKit Console → Deploy Schema Changes | +| Tapping iCloud opens Google Sign-In (or vice versa) | A stale-state race in provider selection | Already fixed in this codebase — see `SECURITY.md`; if you see it again, check you haven't reintroduced a `state.provider` read inside `authenticate()` | +| Unlock screen pops up mid-backup after Google sign-in | Google's native Android picker Activity falsely triggers auto-lock | Already fixed via `lockSuppression.ts` — see `SECURITY.md` | + +### Android + +| Error | Cause | Fix | +|---|---|---| +| `DEVELOPER_ERROR (10)` | SHA-1 not registered, or package name mismatch | Register the correct SHA-1 under the Android OAuth client | +| `authentication failed` / backup write fails | `drive.appdata` scope missing from the access token | Add the scope on the consent screen; re-sign-in afterward | +| `Google Sign-In cancelled` | Person dismissed the sign-in prompt | Normal — they can just retry | +| `Play Services not available` | Device has no Google Play Services | Not supported on that device | + +--- + +## Building for production + +```bash +# iOS +npx expo prebuild --clean +npx expo run:ios --configuration Release --device + +# Android +npx expo prebuild --clean +npx expo run:android --variant release +``` + +For App Store / Play Store submission, use EAS Build: + +```bash +npm install -g eas-cli +eas build --platform ios +eas build --platform android +``` + +Remember: EAS Build signs with its own credentials, separate from your +local debug keystore — register that build's SHA-1 (Android) too, via +`eas credentials`, or Google Sign-In will work locally but fail for +anyone testing a release build. + +--- + +## What's not built yet + +`CloudBackupContext.downloadBackup()` exists and is ready to use, but no +screen calls it yet — restoring a wallet from an existing cloud backup +isn't wired up. The architecture notes on the provider-threading bug and +the auto-lock suppression bug are in `SECURITY.md` if you want the full +technical explanation rather than just the troubleshooting-table summary +above. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..fbb60c8 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing + +## Branch strategy + +Three-tier structure: + +- **`main`** — mirrors upstream `tetherto` reference repo. Don't commit + directly here. +- **`develop`** — the long-lived integration branch. Feature branches + target this. +- **`feature/*`** — one branch per unit of work, branched off `develop`. + +Flow: `feature/*` → PR into `develop` → (eventually) `develop` → `main` for +upstream merge. + +## Before opening a PR + +```bash +npm run typecheck +npm run lint +``` + +If your change touches native config (`app.json` plugins, new native +dependencies), note in your PR description that a `prebuild --clean` is +required — reviewers testing your branch need to know this isn't a +JS-only change. + +## Conventions to follow + +**Read `docs/ARCHITECTURE.md` first.** Several patterns in this codebase +exist specifically to avoid re-introducing bugs that were already found and +fixed — deviating from them without understanding why is how those bugs +come back. + +- **Never import WDK directly in a screen for data reads.** Go through + `@/data`'s hooks. If the data seam doesn't cover what you need yet, + extend it — don't bypass it. +- **All text renders through ``, never raw React Native ``.** + This is what applies responsive font scaling automatically. Same for + `