A browser-extension wallet built to be rented, plus the marketplace that rents it out.
A Yarn monorepo holding the two frontends of the Proxy Wallet project: ProxyWallet, a Manifest V3 Chrome extension wallet, and rent-app, a Next.js marketplace for listing and renting NFTs through it. Both talk to the chainlink-hackathon-contracts SmartWalletV1 / SmartWalletFactoryV1 / NftRent contracts; rent-app additionally reads listing and rental state from chainlink-hackathon-subgraph instead of querying the chain directly.
| Area | Technology |
|---|---|
| Extension | Manifest V3, React 18, TypeScript, Webpack 5, Zustand, Radix UI, LocalForage |
| Marketplace (rent-app) | Next.js 14 (Pages Router), wagmi + viem, Apollo Client (subgraph queries), Tailwind CSS |
| Chain reads/writes | ethers v5 + generated Typechain bindings (extension), viem/wagmi (rent-app) |
| Off-chain data | Moralis (NFT ownership), IPFS gateway resolution (NFT metadata) |
| Tooling | Yarn workspaces, ultra-runner, ESLint, Prettier, Husky + commitlint |
| Question | Mechanism | Package |
|---|---|---|
| How does a webpage talk to the wallet without the wallet trusting the page? | An injected window.proxyWallet.provider object, relayed through a content script to the background service worker |
extension/src/lib/providers, extension/src/lib/message-bridge |
| How is the mnemonic / private key protected at rest? | AES-encrypted in LocalForage, decrypted only in the background worker behind a session password that never touches storage |
extension/src/lib/storage, extension/src/Popup/storageUtils |
| How does the extension turn a mnemonic into a rentable on-chain wallet? | Background methods that call SmartWalletFactoryV1.create2Wallet and cache the resulting address per account |
extension/src/lib/providers/background/methods/internal |
| How does the marketplace know what's listed and who's renting it, without indexing the chain itself? | GraphQL queries against the subgraph via Apollo Client | rent-app/src/hooks/queries |
| How does the marketplace quote a rental's cost in ETH regardless of listing currency? | A live read against the UniswapV3Quoter contract |
rent-app/src/hooks/queries/use-required-native-rent.ts |
| How does the marketplace get an address to transact with? | The same injected-provider request() call any dApp would make |
rent-app/src/hooks/use-request-user-accounts-proxy-wallet.ts |
packages/
├── extension/ # ProxyWallet — Manifest V3 Chrome extension
│ └── src/
│ ├── Background/ # Service worker entry point — message-bridge listener, notifications
│ ├── Content/ # Content script — injects the provider, relays window <-> background
│ ├── Popup/ # Wallet UI: onboarding, home, send / sign / switch-chain flows
│ ├── Options/ Panel/ Devtools/ # Secondary extension surfaces
│ ├── lib/
│ │ ├── providers/ # Injected EIP-1193-style provider + background RPC method implementations
│ │ ├── message-bridge/ # window <-> content-script <-> background transport
│ │ ├── message-handlers/ # Routes an incoming message to its internal/external handler
│ │ └── storage/ # Encrypted mnemonic/private-key storage
│ └── typechain/ # Generated types for SmartWalletV1 / SmartWalletFactoryV1 / UniswapV3Quoter
└── rent-app/ # Next.js marketplace — discover, list, and rent NFTs
└── src/
├── components/pages/index/ # Discover / My NFTs / List NFT tabs
├── hooks/queries/ # Subgraph (Apollo) and on-chain (wagmi) reads
├── constants/ # Per-chain contract addresses and subgraph endpoints
└── pages/api/ # NFT metadata proxy (Moralis + IPFS, with per-network mocks)
flowchart LR
dApp["dApp: window.proxyWallet.provider.request()"] --> Inject["inject.ts (Provider)"]
Inject -->|postMessage| EventBridge[event-bridge.ts]
EventBridge --> ContentScript[content.ts]
ContentScript -->|runtime message| Background[Background service worker]
Background --> Handler[background-message-handler.ts]
Handler -->|external e.g. eth_sendTransaction| ExternalMethods[methods/external]
Handler -->|internal e.g. initializeWallet| InternalMethods[methods/internal]
ExternalMethods --> Popup["Popup UI (user approval)"]
Popup --> Handler
Handler --> ContentScript
ContentScript --> EventBridge
EventBridge --> Inject
Inject --> dApp
The provider sets isMetaMask: true and registers under name: 'ethereum' for compatibility with dApps and libraries that gate wallet features on that flag — it does not claim window.ethereum itself, so it lives alongside other wallets as window.proxyWallet.provider rather than replacing them. External (EIP-1193) methods like eth_sendTransaction route through the Popup for explicit user approval before a background method executes them; internal methods (account/network state, wallet deployment) don't need that round trip.
flowchart LR
RentApp[rent-app] -->|GraphQL query| Subgraph[(chainlink-hackathon-subgraph)]
RentApp -->|price quote| UniswapV3Quoter
RentApp -->|request accounts| ProxyWallet[ProxyWallet extension]
ProxyWallet -->|signs & sends| NftRent[NftRent contract]
NftRent --> SmartWalletFactoryV1
rent-app never talks to the chain to find out what's listed or rented — it queries the subgraph, the same one described in chainlink-hackathon-subgraph. The only on-chain reads it does directly are price-sensitive ones (the Uniswap quote) that need to be current to the block, not eventually-consistent.
nvm install 20 && nvm use 20
npm install --global yarn
yarn installExtension:
cd packages/extension
cp .env.example .env # COINGECKO_API_KEY
yarn startThen in Chrome: remove conflicting wallet extensions, go to chrome://extensions → enable Developer mode → Load unpacked → select packages/extension/build.
Marketplace:
cd packages/rent-app
cp .env.example .env # MORALIS_API_KEY, NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SEPOLIA_GRAPH_URL
yarn devNEXT_PUBLIC_SEPOLIA_GRAPH_URL should point at the subgraph deployed from chainlink-hackathon-subgraph; the contract addresses in packages/rent-app/src/constants/addresses.ts should match whatever chainlink-hackathon-contracts last deployed to that network.
window.proxyWallet.provider, notwindow.ethereum. Claiming the global would fight every other installed wallet for it; a namespaced object avoids that at the cost of dApps needing to know to look for it specifically.- Approval only on external methods. Internal methods (reading the current account, deploying a wallet from inside the extension's own UI) skip the Popup round-trip since they're already user-initiated from inside the extension; external methods (anything a webpage can call) always require it.
- The marketplace trusts the subgraph, not the chain, for listings. This is a deliberate latency/consistency trade-off — subgraph indexing lag is preferable to every page load re-scanning
NftRentevents. - Moralis plus a per-network mock for NFT metadata. Not every network the wallet targets has reliable NFT-metadata indexing yet; the mock path in
pages/api/get-nft-metadata.tskeeps the UI functional on those networks during development.
MIT © RedDuck Limited