個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Noir Wallet — contactless tap-to-pay powered by the x402 protocol on Stellar. Tap an NFC/RFID device and the linked wallet is debited instantly via Soroban smart contracts — no app, no unlock, no confirmation.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Contactless payments powered by Stellar Tap any NFC tag, RFID sticker, or wearable to pay — no app opens, no confirmation, just go. View Pitch Deck · Architecture · Stellar Dev Guide · Deployment Evidence · PR #10 — Week 1 · Run Locally檢視完整 README ↗
Contactless payments powered by Stellar Tap any NFC tag, RFID sticker, or wearable to pay — no app opens, no confirmation, just go.
Docs · View Pitch Deck · Architecture · Stellar Dev Guide · Deployment Evidence · PR #10 — Week 1 · Run Locally
[!IMPORTANT] Noir Wallet is alpha software on Stellar Testnet. The x402 tap-to-pay flow, escrow settlement, and constrained agent authorization are implemented and deployed. Mainnet contracts are deployed but production settlement and PDAX fiat bridge integration are not yet live. Treat testnet deployments as ephemeral — each redeploy is a clean slate with new contract IDs.
▶ Demo — tap-to-pay with the x402 protocol, per-device agent wallets, and the full mobile app. Original X post
▶ A 95-second, Google-style product walkthrough — narrated voiceover, the x402 tap-to-pay flow, per-device agent wallets, and the full mobile app.
Noir Wallet implements the x402 protocol — a zero-interaction payment flow where the wallet is debited immediately upon hardware tap. The user never unlocks their phone, opens an app, or confirms a transaction. The payment terminal reads the device UID, resolves the linked Stellar wallet, and executes the transfer in under 2 seconds.
Built for high-throughput environments: transit turnstiles, campus canteens, event gates, and retail checkout.
A world where:
device_registry for hardware-to-wallet mappingwallet.require_auth()| Layer | Technology |
|---|---|
| Blockchain | Stellar (testnet / mainnet) |
| Smart Contracts | Soroban (Rust) — 3 contracts |
| Frontend | React Native, Expo 57, TypeScript |
| Styling | NativeWind, Tailwind CSS |
| State | Zustand |
| NFC | react-native-nfc-manager |
| Wallet SDK | @stellar/stellar-sdk v16 |
| Testing | Vitest |
| Contract Dev | soroban-cli, Rust nightly |
📐 Full architecture reference — rendered diagrams for the system overview, device provisioning, x402 tap-to-pay, and revocation/fund-recovery flows, plus the authorization model and trust boundaries.
RFID / NFC Tag
|
v
Mobile App / POS Terminal
|
├── NFC read → SHA-256 hash device UID
├── x402 Agent (per-device signing key)
└── Soroban Contracts
|
├── device_registry ──> Map device hash → wallet
├── agent_registry ──> Authorize agent per device
└── payment_escrow ──> Pre-funded escrow → instant authorize
|
v
Stellar Network
|
┌──────┴──────┐
v v
Merchant claim PDAX Fiat Bridge
(batch settle) (PHP Cash-out)
| Contract | Description | Auth | Location |
|---|---|---|---|
| device_registry | Maps hardware device hashes to Stellar wallet addresses | wallet.require_auth() | backend/contracts/device_registry/ |
| agent_registry | On-chain agent authorization per device — allows wallet owner to authorize a signing key for tap-to-pay | wallet.require_auth() | backend/contracts/agent_registry/ |
| payment_escrow | Escrow-based settlement — wallet pre-funds, agent authorizes payments instantly, merchants claim in batch | agent auth (via agent_registry) | backend/contracts/payment_escrow/ |
Testnet Contract IDs (redeployed 2026-10-07 — constrained delegated auth + sweep-on-revoke, #[contractevent] migration):
| Contract | ID | WASM SHA-256 | Explorer |
|---|---|---|---|
| device_registry | CB4DPMGOA374JIB2ZVD4AHW5GJKQUYNOFGRYNOJ75EFH2KCJSMHOWIRA | a252a407…67bea2 | view |
| agent_registry | CBTDMJVCFQDIVWZBKAKZ2FQ3UEJNAAUFYCTJ3E4ON2MYXPLPSIXQ25JZ | b0da4885…c7a22b | view |
| payment_escrow | CA5S4S7QGHJHJWVJBYL3CXZXZTNGKXMNZVDAEQNN7NUXFP4D7BYK7HIX | 3861809c…2941ed | view |
About redeployment: each
./scripts/redeploy-contracts.shrun deploys fresh contract instances, so every deploy produces new contract IDs — a redeploy is a clean slate, not an in-place upgrade (prior on-chain state on the old instances is abandoned). The script writes the authoritative IDs, WASM hashes, and init args to a timestamped evidence file indeploy-evidence/, and when run withUPDATE_ENV=1it also patchesfrontend/.env. The IDs above are from the latest run (deploy-evidence/deploy-testnet-20261007T050558Z.md); on-chain evidence screenshots are indocs/Evidence/.
Full deployment evidence (admin, init args, WASM hashes):
deploy-evidence/.
Mainnet Contract IDs (deployed 2026-07-18):
| Contract | ID | Explorer |
|---|---|---|
| device_registry | CDSURGM4LYYRZ6U4RKBRUQPA7SGLAJZ5XXS65ACYCIU6QOO2NEEA2S45 | view |
| agent_registry | CDE2Q22BNKHLRNHUXHNL4TYRN5YPCOCO2XD53MAVYG7K2C4KZ2T6NI5T | view |
| payment_escrow | CCEIB2BLMHK7N7OX23HJ3RCMJBP2NLBMMBGUA6SB5IWJJ6JEWH6ZZPK3 | view |
| Method | Args | Auth | Description |
|---|---|---|---|
initialize | admin: Address | admin | Set contract admin (called once) |
register | wallet: Address, device_hash: BytesN<32>, agent: Address | wallet | Register a device to a wallet with its agent |
revoke | wallet: Address, device_hash: BytesN<32> | wallet | Remove the device entry (owner only), freeing the hash for re-registration |
get_device | device_hash: BytesN<32> | — | Returns DeviceInfo { owner, agent, status, created_at } |
wallet_device_count | wallet: Address | — | How many devices a wallet has registered |
wallet_device_at | wallet: Address, index: u32 | — | Enumerate a wallet's device hashes |
is_authorized | device_hash: BytesN<32>, agent: Address | — | true if the device exists, is active, and maps to this agent |
get_agent | device_hash: BytesN<32> | — | Agent for a device |
get_owner | device_hash: BytesN<32> | — | Owner for a device |
Errors: AlreadyInitialized=1, DeviceNotFound=2, NotOwner=3, AlreadyRegistered=4.
| Method | Args | Auth | Description |
|---|---|---|---|
initialize | admin: Address | admin | Set contract admin (called once) |
register_agent | wallet: Address, device_hash: BytesN<32>, agent: Address, max_amount: i128, asset: Address, expires_at: u64 | wallet | Authorize an agent under a constrained policy |
revoke_agent | wallet: Address, device_hash: BytesN<32> | wallet | Revoke agent access |
get_agent | device_hash: BytesN<32> | — | Look up agent by device hash |
get_policy | device_hash: BytesN<32> | — | Returns AgentPolicy { agent, max_amount, asset, expires_at } |
is_auth | device_hash: BytesN<32>, agent: Address | — | Agent matches and authorization has not expired |
check_payment | device_hash: BytesN<32>, agent: Address, asset: Address, amount: i128 | — | Full policy predicate: agent + asset + amount > 0 + within cap + not expired |
max_amount = 0 means uncapped; expires_at = 0 means never expires (otherwise an absolute ledger timestamp).
Errors: AlreadyInitialized=1, AgentNotFound=2, AlreadyRegistered=3, InvalidPolicy=4.
| Method | Args | Auth | Description |
|---|---|---|---|
initialize | admin: Address, agent_registry_id: Address, device_registry_id: Address | admin | Set admin + link both registries |
fund_escrow | token: Address, wallet: Address, device_hash: BytesN<32>, amount: i128 | wallet | Deposit into escrow for a device |
authorize | agent: Address, device_hash: BytesN<32>, merchant: Address, asset: Address, amount: i128, nonce: u64 | agent | Instant payment auth, enforced against the agent policy with nonce replay protection |
claim | token: Address, merchant: Address | merchant | Batch-claim all pending payments |
defund_escrow | token: Address, device_hash: BytesN<32>, amount: i128 | device owner | Wallet reclaims unused escrow funds |
sweep_on_revoke | token: Address, device_hash: BytesN<32>, agent: Address | device owner | After the agent is revoked, return the device's entire escrow balance to the owner |
balance_of | device_hash: BytesN<32> | — | Check escrow balance |
pending_balance | merchant: Address | — | Check unclaimed payments |
Errors: AlreadyInitialized=1, InsufficientBalance=2, AgentNotAuthorized=3, NotDeviceOwner=4, NothingToClaim=5, DuplicateAuth=6, AgentStillActive=7.
Noir_Wallet/
├── frontend/ # React Native Expo application
│ └── src/
│ ├── screens/ # Dashboard, POS, Device Provisioning, Agents, etc.
│ ├── components/ # BalanceCard, NumericKeypad, ReadyToTap, etc.
│ ├── services/ # Stellar SDK, NFC, API client
│ ├── store/ # Zustand state management
│ ├── hooks/ # useNfc, useProfile, custom hooks
│ ├── lib/ # soroban.ts helpers, x402 auth
│ ├── domain/ # x402 agent logic
│ ├── tasks/ # Background tasks (received-payment alerts)
│ ├── constants/ # Theme (black/gold), network config
│ └── types/ # TypeScript type definitions
├── backend/ # Soroban smart contracts (Rust)
│ └── contracts/
│ ├── device_registry/
│ ├── agent_registry/
│ └── payment_escrow/
├── assets/ # Demo video, poster, branding
├── images/ # Screenshots & diagrams
├── promo/ # Promotional materials
├── models/ # ML / design models
├── old/ # Archived code (backward compat)
├── unused/ # Parked, not-in-use components
│ └── pdax-backend/ # PDAX fiat-bridge API server (parked — not wired into the app)
└── contextimages/ # Design inspiration and moodboards
device_registry.register() on Soroban, and polls for confirmationagent_registry.register_agent() to authorize the agent for this devicepayment_escrow (one-time, per device)payment_escrow.authorize(merchant, amount)payment_escrow.claim() in batch — one transaction claims all pending paymentsdefund_escrow() unused balance at any timeNoir Wallet notifies you when money arrives at your main wallet or at any card's agent wallet, including when you are not in the app. The app needs no server for this: it reads your accounts' payments from Horizon directly.
| App state | How it checks | Delay |
|---|---|---|
| Open | Balances update live on screen; on opening, anything that arrived while you were away is notified | — |
| In the background (still running) | Polls Horizon every 30 seconds | ≤ 30 s |
| Closed or swiped away | expo-background-task job (Android WorkManager) | ~15 min or more; Android decides when it runs |
What notifies: incoming XLM or asset payments, and create_account funding (e.g. Friendbot), from someone else. Each notification reads like "Received 50.00 XLM — From GSTR…XXX3 to Black card".
What doesn't: payments you send, failed transactions, and moves between your own accounts (topping up a card from your main wallet, or a card's balance returning to it). Old payments never flood in: the first check for an account only records where it is, and each payment notifies once.
How it works
index.ts (the app entry) imports src/tasks/paymentTask.ts before the router, which defines the background task so Android can run it while the app is closed.app/_layout.tsx creates the Android Payments notification channel, asks for notification permission, takes a first snapshot of each account, and registers the background task.checkIncomingPayments in src/services/paymentNotifier.ts) fetches the latest Horizon payments for the main wallet and each card agent, keeps the ones newer than the last seen paging_token (stored per network and account in AsyncStorage), and shows one notification per payment. The filtering and wording live in src/lib/incomingPayments.ts and are unit-tested in tests/17-incoming-payments.test.ts.Limits to know
npx expo prebuild --platform android, then npx expo run:android) after pulling this; a JS reload is not enough.Test it: open the app once so it snapshots your accounts and asks for permission. Leave it in the background and send XLM to your wallet address from another account (e.g. Stellar Laboratory on Testnet); a notification appears within about 30 seconds. To test the closed-app path without waiting, call BackgroundTask.triggerTaskWorkerForTestingAsync() from a dev build.
[!TIP] Full guides: Installation · Deployment · Walkthrough · Contract reference · API reference · NFC hardware · Testing. The summary below is the short version.
npm install -g expo-cli)# Clone and install
git clone https://github.com/rylsherdamz-rgb/Noir_Wallet.git
cd Noir_Wallet/frontend
npm install
Copy .env.example to .env and configure:
| Variable | Description | Current Value |
|---|---|---|
EXPO_PUBLIC_DEVICE_REGISTRY_CONTRACT | Soroban device registry contract ID | CB4DPMGOA374JIB2ZVD4AHW5GJKQUYNOFGRYNOJ75EFH2KCJSMHOWIRA |
EXPO_PUBLIC_AGENT_REGISTRY_CONTRACT | Soroban agent registry contract ID | CBTDMJVCFQDIVWZBKAKZ2FQ3UEJNAAUFYCTJ3E4ON2MYXPLPSIXQ25JZ |
EXPO_PUBLIC_PAYMENT_ESCROW_CONTRACT | Soroban payment escrow contract ID | CA5S4S7QGHJHJWVJBYL3CXZXZTNGKXMNZVDAEQNN7NUXFP4D7BYK7HIX |
EXPO_PUBLIC_STELLAR_MASTER_KEY_ID | Stellar master key ID | (configure per deployment) |
EXPO_PUBLIC_CHANNEL_SECRET_KEY | Fee channel secret | (configure per deployment) |
EXPO_PUBLIC_ISSUER_ADDRESS | Asset issuer address | (configure per deployment) |
EXPO_PUBLIC_PDAX_API_KEY | PDAX sandbox/prod API key | (optional) |
EXPO_PUBLIC_TERMINAL_ID | x402 Terminal ID | (optional) |
cd frontend
npx expo start
NFC needs native code, so Expo Go and emulators can't link or tap cards. On an NFC Android phone, build a development client with npx expo run:android (see Installation).
cd backend
# Build all WASM
cargo build --release --target wasm32v1-none -p device-registry -p agent-registry -p payment-escrow
# Run checks (host target)
cargo check -p device-registry -p agent-registry -p payment-escrow
# Deploy to testnet (one per contract)
soroban contract deploy \
--wasm target/wasm32v1-none/release/device_registry.wasm \
--network testnet
soroban contract deploy \
--wasm target/wasm32v1-none/release/agent_registry.wasm \
--network testnet
soroban contract deploy \
--wasm target/wasm32v1-none/release/payment_escrow.wasm \
--network testnet
# Initialize contracts
soroban contract invoke --id <DEVICE_REGISTRY_ID> --network testnet -- \
initialize --admin <ADMIN_ADDR>
soroban contract invoke --id <AGENT_REGISTRY_ID> --network testnet -- \
initialize --admin <ADMIN_ADDR>
soroban contract invoke --id <ESCROW_ID> --network testnet -- \
initialize --admin <ADMIN_ADDR> --agent_registry_id <AGENT_REGISTRY_ID> --device_registry_id <DEVICE_REGISTRY_ID>
Build agent-registry and device-registry before payment-escrow (it imports their WASM). Or run all of this with ./scripts/redeploy-contracts.sh; see the Deployment guide.
cd frontend
npm test
Contract, SDK and live Testnet tests: see Testing.
We collected structured feedback from 60 testers who registered devices, funded escrows, and completed payment flows on Stellar testnet. The results shaped our roadmap — here's what mattered most to them.
Top requested improvements:
| Rank | Request | Why it mattered |
|---|---|---|
| 1 | Onboarding tutorial | Step-by-step guide for device provisioning and first payment |
| 2 | QR code scanner | Scan wallet addresses on receive and send screens |
| 3 | Dark mode toggle | Theme switch for late-night use |
| 4 | Transaction search & filters | Find payments by date, amount, or counterparty |
| 5 | Push notifications | Payment confirmations and escrow alerts |
| 6 | Better empty states | Sample data or guided prompts on first launch |
| 7 | Haptic feedback | Vibration on successful NFC tap |
| 8 | Custom NFC device names | Rename tags instead of showing hashes |
| 9 | Multi-currency display | Show USD/PHP equivalent alongside XLM |
| 10 | Spending limits per device | Configurable per-NFC-card budgets |
💡 All ten improvements are shortlisted for the v1 roadmap — starting with the onboarding tutorial and NFC device naming.
Contributions are welcome. Before you start, please read:
Use the GitHub issue templates to report bugs or request features, and the PR template when opening pull requests.
Promotional assets and a browser-based pubmat editor live in promo/pubmat/.
| Role | Name |
|---|---|
| Fullstack Developer | Richie Christian De Guzman |
| Backend Developer | Johnrick Rabara |
| UI/UX Designer | Jefferson Tuparan |
MIT