repository cleanup and scrollbar fix

This commit is contained in:
viraladmin 2026-08-02 23:31:44 -06:00
parent cd531a2218
commit f181442c67
26 changed files with 244 additions and 770 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
core/target/
extension/node_modules/
extension/dist/
extension/web-ext-artifacts/
tests/storage/dual-signatures/

142
PRIVACY_POLICY.md Normal file
View File

@ -0,0 +1,142 @@
# Contractless Web3 Wallet Privacy Policy
Effective date: August 2, 2026
Contractless Web3 Wallet is a browser extension for creating and managing
Contractless blockchain wallets, connecting to compatible Web3 applications,
and reviewing, signing, and broadcasting Contractless transactions.
## Information Contractless Does Not Collect
Contractless Web3 Wallet does not include advertising, analytics, telemetry,
tracking, or behavioral profiling. The developer does not collect or sell:
- names, email addresses, telephone numbers, or other identifying information;
- browsing history or general website activity;
- health information;
- personal communications;
- precise or approximate location information; or
- wallet encryption keys or private keys.
## Information Processed Locally
The wallet must process financial and authentication information to provide its
requested functionality. This processing occurs locally inside the browser
extension and does not make the information available to the developer.
Locally processed information may include:
- encrypted Contractless wallet files;
- wallet addresses, public keys, and vanity addresses;
- private keys and wallet encryption keys while the wallet is unlocked;
- balances, assets, NFTs, RWAs, loans, and transaction history;
- unsigned and signed Contractless transactions;
- website connection approvals and temporary application access keys; and
- wallet labels, network selection, API endpoints, and automatic-lock settings.
Private keys are encrypted before being placed in persistent extension storage.
Decrypted private keys exist only while the wallet is unlocked. Locking the
wallet clears decrypted wallet material and expires active website sessions.
## Contractless API Communication
The extension communicates with the Contractless API selected in the wallet's
settings. Testnet defaults to `https://api.contractless.dev`. Users may replace
the API endpoint, and mainnet uses a separately configured endpoint.
The extension sends only the information required to perform a user-requested
blockchain operation. Depending on the request, this may include:
- a wallet address, public key, and authentication signature;
- an address, asset identifier, transaction identifier, or block identifier
required for a blockchain lookup; or
- a complete signed transaction submitted for validation and broadcast.
Private keys and wallet encryption keys are never sent to a Contractless API,
website, blockchain node, or the extension developer.
An API endpoint chosen by the user is operated independently and may have its
own privacy and logging practices. Users should only configure API endpoints
they trust.
## Website Connections
Contractless-compatible websites may request access to the wallet through the
Contractless Web3 provider. A website does not receive the active wallet address
until the user explicitly approves its connection request.
After approval, the website receives a temporary access key associated with
that website, application identifier, wallet, and unlock session. The access
expires when the wallet locks or the user disconnects the application.
Websites may request message signatures or transactions. The wallet displays
the request for review and requires user approval before signing. A connected
website receives only the address, signatures, transaction information, and
other results required to complete the approved request. Websites never receive
the wallet private key or encryption key.
Contractless does not control third-party websites. Their handling of
information received after user approval is governed by their own privacy
policies.
## Browser Permissions
Contractless Web3 Wallet uses the following browser permissions:
- **Storage:** Stores encrypted wallets, wallet settings, labels, and temporary
application sessions.
- **Alarms:** Enforces the user-configured automatic wallet lock timeout.
- **Clipboard write:** Copies information only after the user selects a copy
action, such as copying an address or transaction identifier.
- **Host access:** Makes the Contractless provider available to compatible
websites and communicates with the configured Contractless API. The extension
does not read or collect general webpage content.
## Data Retention and Deletion
Wallet information remains in browser extension storage until the user removes
it, clears the extension's storage, or uninstalls the extension. Temporary
website access information expires when the wallet locks or the application is
disconnected.
Contractless does not maintain a separate developer-controlled database of
extension wallets or user activity.
## Sharing and Sale of Information
Contractless does not sell user information. Information is not used for
advertising, credit decisions, behavioral profiling, or purposes unrelated to
the wallet's user-facing functionality.
Information is transmitted only when necessary to complete a user-requested
wallet operation, communicate with the configured Contractless API, or respond
to an explicitly approved website request.
The use of information by Contractless Web3 Wallet is limited to providing and
improving its disclosed wallet functionality. Information is not transferred
for advertising or other unrelated purposes, and humans are not permitted to
read private wallet information except when specifically requested by the user
for support, required for security, or required by law.
## Security
Contractless Web3 Wallet performs cryptographic operations locally using code
and WebAssembly bundled with the extension. API communication must use HTTPS,
except when a user deliberately configures a local development endpoint.
No system can guarantee absolute security. Users are responsible for protecting
their wallet images, private keys, encryption keys, devices, and configured API
endpoints.
## Changes to This Policy
This policy may be updated when wallet functionality or privacy requirements
change. Material changes will be reflected by updating this document and its
effective date.
## Contact
Questions or privacy concerns may be submitted through the Contractless Web3
Wallet repository:
https://contractless.dev/contractless/Contractless-Web3-Wallets

View File

@ -89,6 +89,9 @@ Private keys remain inside the extension. Websites receive only approved
addresses, signatures, signed transaction bytes, and session-scoped access
keys.
The wallet's data-handling practices are described in the
[Privacy Policy](PRIVACY_POLICY.md).
## Integration Test
The `tests/tests.html` page verifies that a website can detect the extension,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
*{box-sizing:border-box}body{max-width:760px;margin:0 auto;padding:34px;color:#111820;font:14px/1.45 Arial,Helvetica,sans-serif}header{display:flex;align-items:center;gap:15px;padding-bottom:18px;border-bottom:3px solid #2167a5}header img{width:58px}h1{margin:0;font-size:24px}header p{margin:2px 0 0;color:#5c6972}.warning{margin:22px 0;border:2px solid #9b1c1c;padding:12px;color:#7a1111;font-weight:700}section{margin-top:18px}h2{margin:0 0 7px;color:#42515b;font-size:12px;text-transform:uppercase}pre{margin:0;overflow-wrap:anywhere;white-space:pre-wrap;border:1px solid #aebac2;padding:11px;font:10px/1.35 Consolas,monospace}.print-error{color:#9b1c1c;font-weight:700}@media print{body{max-width:none;padding:0}}

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"permissions": ["storage", "alarms", "activeTab", "clipboardWrite"],
"permissions": ["storage", "alarms", "clipboardWrite"],
"host_permissions": ["https://api.contractless.dev/*"],
"optional_host_permissions": ["https://*/"],
"action": {

View File

@ -7,7 +7,7 @@
<script type="module" crossorigin src="/popup.js"></script>
<link rel="modulepreload" crossorigin href="/chunks/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="modulepreload" crossorigin href="/chunks/core-D2IK1U91.js">
<link rel="stylesheet" crossorigin href="/assets/popup-B3vY8vDf.css">
<link rel="stylesheet" crossorigin href="/assets/popup-DTeT6cow.css">
</head>
<body>
<main class="wallet-shell">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();

View File

@ -1 +0,0 @@
const o=document.createElement("script");o.src=chrome.runtime.getURL("provider.js");o.onload=()=>o.remove();(document.head||document.documentElement).appendChild(o);window.addEventListener("message",e=>{var t;e.source!==window||((t=e.data)==null?void 0:t.source)!=="contractless-page"||chrome.runtime.sendMessage({type:"provider-request",method:e.data.method,params:e.data.params},a=>{window.postMessage({source:"contractless-wallet",id:e.data.id,response:a},window.location.origin)})});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -18,7 +18,7 @@
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"permissions": ["storage", "alarms", "activeTab", "clipboardWrite"],
"permissions": ["storage", "alarms", "clipboardWrite"],
"host_permissions": ["https://api.contractless.dev/*"],
"optional_host_permissions": ["https://*/"],
"action": {

View File

@ -1,45 +0,0 @@
{
"manifest_version": 3,
"name": "Contractless Wallet",
"version": "0.1.1",
"description": "A local-signing Web3 wallet for Contractless.",
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"permissions": ["storage", "alarms", "activeTab", "clipboardWrite"],
"host_permissions": ["https://api.contractless.dev/*"],
"optional_host_permissions": ["https://*/"],
"action": {
"default_popup": "popup.html",
"default_title": "Contractless Wallet",
"default_icon": {
"16": "contractless-logo.png",
"32": "contractless-logo.png",
"48": "contractless-logo.png",
"128": "contractless-logo.png"
}
},
"icons": {
"16": "contractless-logo.png",
"32": "contractless-logo.png",
"48": "contractless-logo.png",
"128": "contractless-logo.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": ["provider.js"],
"matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"]
}
]
}

View File

@ -1,635 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Contractless Wallet</title>
<script type="module" crossorigin src="/popup.js"></script>
<link rel="modulepreload" crossorigin href="/chunks/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="modulepreload" crossorigin href="/chunks/core-CvwXoH-U.js">
<link rel="stylesheet" crossorigin href="/assets/popup-C5oUQoSX.css">
</head>
<body>
<main class="wallet-shell">
<section id="loading" class="screen loading-screen">
<div class="loading-brand">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="Contractless" />
<div class="spinner" aria-label="Loading"></div>
</div>
<p>Opening wallet</p>
</section>
<section id="create-screen" class="screen" hidden>
<header class="wallet-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small>Browser Wallet</small></div>
<span>1 of 3</span>
</header>
<div class="screen-content">
<span class="eyebrow">New wallet</span>
<h1>Create your wallet</h1>
<p class="intro">Choose an encryption key used to unlock and recover this wallet.</p>
<label for="new-password">Encryption key</label>
<div class="input-row">
<input id="new-password" type="password" autocomplete="new-password" />
<button class="reveal" type="button" data-input="new-password">Show</button>
</div>
<div class="note">
<strong>This key is not stored for you.</strong>
<span>You will verify it before the wallet is created.</span>
</div>
<button id="continue-create" class="primary" type="button">Continue <span></span></button>
<div class="divider"><span>or restore an existing wallet</span></div>
<button class="secondary navigation" data-screen="image-screen" type="button">
Import wallet from image
</button>
<button class="secondary navigation" data-screen="private-screen" type="button">
Import wallet from private key
</button>
<button id="cancel-wallet-add" class="text-button" type="button" hidden>
Cancel
</button>
</div>
</section>
<section id="image-screen" class="screen" hidden>
<header class="wallet-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small>Browser Wallet</small></div>
<span>Import</span>
</header>
<div class="screen-content">
<span class="eyebrow">Existing wallet</span>
<h1>Import wallet image</h1>
<p class="intro">
Select your encrypted private-key image and enter the encryption key
originally used to create it.
</p>
<label for="image-password">Encryption key</label>
<div class="input-row">
<input id="image-password" type="password" autocomplete="current-password" />
<button class="reveal" type="button" data-input="image-password">Show</button>
</div>
<label class="file-heading" for="wallet-image">Wallet image</label>
<label class="file-picker" for="wallet-image">
<span class="file-icon">+</span>
<span><strong id="image-file-name">Choose wallet image</strong><small>PNG encrypted private-key image</small></span>
</label>
<input id="wallet-image" class="visually-hidden" type="file" accept="image/png" />
<button id="import-image" class="primary" type="button">Import wallet</button>
<button class="text-button navigation" data-screen="create-screen" type="button">Back</button>
</div>
</section>
<section id="private-screen" class="screen" hidden>
<header class="wallet-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small>Browser Wallet</small></div>
<span>Import</span>
</header>
<div class="screen-content compact">
<span class="eyebrow">Existing wallet</span>
<h1>Import private key</h1>
<p class="intro">
Paste the private key, then choose the encryption key that will protect
this wallet from now on.
</p>
<label for="private-key">Private key</label>
<textarea id="private-key" rows="4" placeholder="Paste private key"></textarea>
<label for="private-password">New encryption key</label>
<div class="input-row">
<input id="private-password" type="password" autocomplete="new-password" />
<button class="reveal" type="button" data-input="private-password">Show</button>
</div>
<div class="note compact-note">
<strong>This key protects the new wallet image.</strong>
<span>Store it securely and separately from the image.</span>
</div>
<button id="import-private" class="primary" type="button">Import and create wallet</button>
<button class="text-button navigation" data-screen="create-screen" type="button">Back</button>
</div>
</section>
<section id="verify-screen" class="screen" hidden>
<header class="wallet-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small>Browser Wallet</small></div>
<span>2 of 3</span>
</header>
<div class="screen-content">
<span class="eyebrow">Recovery</span>
<h1>Verify your encryption key</h1>
<p class="intro">
Enter the same key again. Keep it somewhere secure and separate from
your wallet image.
</p>
<label for="verify-password">Re-enter encryption key</label>
<div class="input-row">
<input id="verify-password" type="password" autocomplete="new-password" />
<button class="reveal" type="button" data-input="verify-password">Show</button>
</div>
<div class="warning">
<strong>There is no password reset.</strong>
Losing this key means the wallet image cannot restore your wallet.
</div>
<button id="create-wallet" class="primary" type="button">Create wallet <span></span></button>
<button class="text-button navigation" data-screen="create-screen" type="button">Back</button>
</div>
</section>
<section id="backup-screen" class="screen" hidden>
<header class="wallet-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small>Browser Wallet</small></div>
<span>3 of 3</span>
</header>
<div class="screen-content backup-content">
<span class="success-mark"></span>
<h1>Your wallet is ready</h1>
<p class="intro">
Save the private-key image now. You need both this image and your
encryption key to recover the wallet.
</p>
<div class="image-preview">
<img id="wallet-image-preview" alt="Encrypted private-key wallet image" />
</div>
<button id="save-image" class="primary" type="button">↓ Save wallet image</button>
<label class="confirmation">
<input id="backup-confirmed" type="checkbox" disabled />
<span>I saved the image and stored my encryption key separately.</span>
</label>
<button id="open-wallet" class="secondary" type="button" disabled>Open wallet</button>
</div>
</section>
<section id="locked-screen" class="screen" hidden>
<form id="unlock-form" class="centered-content">
<img class="large-logo" src="/assets/contractless-logo-BgfMS-Kf.png" alt="Contractless" />
<h1>Unlock wallet</h1>
<p class="intro">Enter your encryption key to continue.</p>
<label for="unlock-password">Encryption key</label>
<div class="input-row">
<input id="unlock-password" type="password" autocomplete="current-password" />
<button class="reveal" type="button" data-input="unlock-password">Show</button>
</div>
<button id="unlock-wallet" class="primary" type="submit">Unlock wallet</button>
</form>
</section>
<section id="provider-approval-screen" class="screen provider-approval-screen" hidden>
<header class="wallet-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small>Wallet request</small></div>
<span>Review</span>
</header>
<div class="screen-content">
<div class="approval-context">
<span class="eyebrow" id="provider-approval-origin"></span>
<span id="provider-approval-risk" class="risk-label"></span>
</div>
<h1 id="provider-approval-title">Review request</h1>
<p class="intro" id="provider-approval-intro">Review this request before approving it.</p>
<dl id="provider-approval-details" class="approval-details"></dl>
</div>
<div class="approval-actions">
<button id="reject-provider-approval" class="secondary" type="button">Reject</button>
<button id="approve-provider-approval" class="primary" type="button">Approve</button>
</div>
</section>
<section id="dashboard-screen" class="screen" hidden>
<header class="wallet-header dashboard-header">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div><strong>Contractless</strong><small data-network-name>Testnet</small></div>
<button
id="account-menu-button"
class="account-button"
type="button"
title="Wallets and settings"
aria-expanded="false"
aria-controls="account-drawer"
>
<span class="network-dot"></span>
<span id="wallet-address-short">Account</span>
</button>
</header>
<div class="assets-dashboard">
<nav class="wallet-actions" aria-label="Wallet actions">
<button id="open-send" type="button">
<span class="action-icon"></span>
<span>Send</span>
</button>
<button id="open-receive" type="button">
<span class="action-icon"></span>
<span>Receive</span>
</button>
<button id="open-nfts" type="button">
<span class="action-icon">#</span>
<span>NFTs/RWAs</span>
</button>
<button id="open-activity" type="button">
<span class="action-icon"></span>
<span>Activity</span>
</button>
</nav>
<section class="portfolio-balance" aria-labelledby="portfolio-title">
<span id="portfolio-title">Available balance</span>
<strong><span id="base-balance">0.00000000</span> <small data-network-symbol>CLTC</small></strong>
<p id="wallet-address"></p>
</section>
<section class="asset-ledger" aria-labelledby="assets-title">
<div class="asset-ledger-heading">
<h1 id="assets-title">Assets</h1>
<button id="refresh-balances" type="button" title="Refresh balances" aria-label="Refresh balances"></button>
</div>
<div class="asset-filter" role="group" aria-label="Token visibility">
<button class="active" data-token-filter="held" type="button">Held tokens</button>
<button data-token-filter="all" type="button">All tokens</button>
</div>
<div class="asset-row base-asset">
<div class="asset-symbol contractless-symbol">
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
</div>
<div class="asset-name">
<strong id="base-asset-name">Contractless Testnet</strong>
<span data-network-symbol>CLTC</span>
</div>
<div class="asset-value">
<strong id="base-asset-balance">0.00000000</strong>
<span data-network-symbol>CLTC</span>
</div>
</div>
<div id="token-list" class="token-list">
<div class="empty-assets">
<strong>Loading token balances...</strong>
<span>Balances are retrieved from the selected Contractless API.</span>
</div>
</div>
</section>
</div>
</section>
<section id="nfts-screen" class="screen nfts-screen" hidden>
<header class="wallet-header">
<button id="close-nfts" class="header-back" type="button" title="Back to assets" aria-label="Back to assets">&larr;</button>
<div><strong>NFTs and RWAs</strong><small>Assets owned by this wallet</small></div>
<button id="refresh-nfts" class="header-refresh" type="button" title="Refresh assets" aria-label="Refresh assets">&#8635;</button>
</header>
<div class="nfts-content">
<div class="nfts-summary">
<span id="nfts-count">Loading ownership...</span>
<span data-network-name>Testnet</span>
</div>
<div id="nfts-list" class="nfts-list"></div>
<div id="nfts-pagination" class="nfts-pagination" hidden>
<button id="nfts-previous" type="button" aria-label="Previous NFT page">&larr;</button>
<span>Page <strong id="nfts-page">1</strong> of <strong id="nfts-pages">1</strong></span>
<button id="nfts-next" type="button" aria-label="Next NFT page">&rarr;</button>
</div>
</div>
</section>
<section id="nft-detail-screen" class="screen nft-detail-screen" hidden>
<header class="wallet-header">
<button id="close-nft-detail" class="header-back" type="button" title="Back to NFTs and RWAs" aria-label="Back to NFTs and RWAs">&larr;</button>
<div><strong id="nft-detail-title">NFT/RWA</strong><small id="nft-detail-kind">On-chain asset</small></div>
<span data-network-name>Testnet</span>
</header>
<div class="nft-detail-content">
<div class="nft-detail-media">
<img id="nft-detail-image" alt="" />
</div>
<div class="nft-detail-heading">
<div>
<span id="nft-detail-badge">NFT</span>
<h1 id="nft-detail-name">NFT/RWA</h1>
</div>
<strong id="nft-detail-ownership"></strong>
</div>
<p id="nft-detail-description" class="nft-detail-description"></p>
<div id="nft-detail-attributes" class="nft-detail-attributes"></div>
<dl id="nft-detail-fields" class="nft-detail-fields"></dl>
<div class="nft-detail-actions">
<button id="transfer-nft" class="primary" type="button">Transfer asset</button>
</div>
<section class="nft-provenance">
<h2>Provenance</h2>
<div id="nft-detail-history"></div>
</section>
</div>
</section>
<section id="send-screen" class="screen send-screen" hidden>
<header class="wallet-header">
<button id="close-send" class="header-back" type="button" title="Back to assets" aria-label="Back to assets"></button>
<div><strong>Send</strong><small>Create a transfer</small></div>
<span data-network-name>Testnet</span>
</header>
<form id="send-form" class="send-content">
<div>
<label for="send-asset">Asset</label>
<select id="send-asset"></select>
</div>
<div>
<label for="send-recipient">Receiving address</label>
<input id="send-recipient" type="text" autocomplete="off" spellcheck="false" placeholder="Wallet or vanity address" />
</div>
<div>
<label for="send-amount">Amount</label>
<div class="send-amount-row">
<input id="send-amount" type="text" inputmode="decimal" autocomplete="off" placeholder="0.00000000" />
<span id="send-amount-symbol" data-network-symbol>CLTC</span>
</div>
<span id="send-available" class="field-note">Available: 0.00000000 CLTC</span>
</div>
<div>
<label for="send-fee">Transaction fee</label>
<div class="send-amount-row">
<input id="send-fee" type="text" inputmode="decimal" autocomplete="off" placeholder="0.00000000" />
<span data-network-symbol>CLTC</span>
</div>
<span id="send-fee-note" class="field-note">Minimum fee: 1% of the transfer amount.</span>
</div>
<button class="primary send-continue" type="submit">Review transfer</button>
</form>
</section>
<section id="send-review-screen" class="screen send-review-screen" hidden>
<header class="wallet-header">
<button id="edit-send" class="header-back" type="button" title="Edit transfer" aria-label="Edit transfer"></button>
<div><strong>Review transfer</strong><small>Confirm before signing</small></div>
<span data-network-name>Testnet</span>
</header>
<div class="send-review-content">
<div class="send-review-amount">
<span>You are sending</span>
<strong><span id="review-amount">0.00000000</span> <small id="review-asset">CLTC</small></strong>
</div>
<dl class="send-review-details">
<div><dt>From</dt><dd id="review-sender"></dd></div>
<div><dt>To</dt><dd id="review-recipient"></dd></div>
<div><dt>Resolved address</dt><dd id="review-resolved-recipient"></dd></div>
<div><dt>Transaction fee</dt><dd><span id="review-fee"></span> <span data-network-symbol>CLTC</span></dd></div>
<div><dt>Total deducted</dt><dd id="review-total"></dd></div>
</dl>
<p class="signing-notice">Your private key remains inside this wallet. The transaction is signed locally and only the completed transaction is sent to the API.</p>
<button id="sign-broadcast-transfer" class="primary" type="button">Sign and broadcast</button>
</div>
</section>
<section id="send-success-screen" class="screen send-success-screen" hidden>
<header class="wallet-header">
<div><strong>Transfer broadcast</strong><small>Submitted to Contractless</small></div>
<span data-network-name>Testnet</span>
</header>
<div class="send-success-content">
<span class="success-mark"></span>
<h1>Transaction sent</h1>
<p>The signed transfer was accepted for broadcast.</p>
<div class="send-txid">
<span>Transaction ID</span>
<strong id="send-result-txid"></strong>
</div>
<button id="copy-send-txid" class="secondary" type="button">Copy transaction ID</button>
<button id="finish-send" class="primary" type="button">Return to assets</button>
</div>
</section>
<section id="activity-screen" class="screen activity-screen" hidden>
<header class="wallet-header">
<button id="close-activity" class="header-back" type="button" title="Back to assets" aria-label="Back to assets"></button>
<div><strong>Activity</strong><small>Latest 25 transactions</small></div>
<span data-network-name>Testnet</span>
</header>
<div class="activity-content">
<div class="activity-summary">
<span>Newest transactions first</span>
<div>
<strong id="activity-count">0 shown</strong>
<button id="refresh-activity" type="button" title="Refresh activity" aria-label="Refresh activity"></button>
</div>
</div>
<div id="activity-list" class="activity-list"></div>
</div>
</section>
<section id="transaction-screen" class="screen transaction-screen" hidden>
<header class="wallet-header">
<button id="close-transaction" class="header-back" type="button" title="Back to activity" aria-label="Back to activity"></button>
<div><strong id="transaction-title">Transaction</strong><small>Complete on-chain details</small></div>
<span id="transaction-header-status"></span>
</header>
<div class="transaction-content">
<section class="transaction-identity">
<div>
<span>Transaction ID</span>
<strong id="transaction-full-txid"></strong>
</div>
<button id="copy-transaction-txid" type="button">Copy TXID</button>
</section>
<div id="transaction-overview" class="transaction-fields"></div>
<section class="transaction-section">
<h2>Transaction Fields</h2>
<div id="transaction-fields" class="transaction-fields"></div>
</section>
<section id="miner-earnings-section" class="transaction-section" hidden>
<h2>Miner Earnings</h2>
<div id="transaction-miner-earnings" class="transaction-fields"></div>
</section>
<details class="transaction-technical">
<summary>Technical Details</summary>
<div id="transaction-technical-fields" class="transaction-fields"></div>
</details>
</div>
</section>
<section id="receive-screen" class="screen receive-screen" hidden>
<header class="wallet-header">
<button id="close-receive" class="header-back" type="button" title="Back to assets" aria-label="Back to assets"></button>
<div><strong>Receive</strong><small>Contractless wallet address</small></div>
<span data-network-name>Testnet</span>
</header>
<div class="receive-content">
<span class="receive-label">Receive CLTC and Contractless assets</span>
<h1>Share your address</h1>
<p>
Scan the QR code or copy the address below. Only send assets issued on
the Contractless testnet to this address.
</p>
<div class="qr-frame">
<canvas id="receive-qr" width="220" height="220" aria-label="Wallet address QR code"></canvas>
</div>
<div class="receive-address">
<span id="receive-address-label">Wallet address</span>
<strong id="receive-address-value"></strong>
</div>
<button id="copy-receive-address" class="primary receive-copy" type="button">
Copy address
</button>
</div>
</section>
<div id="drawer-backdrop" class="drawer-backdrop" hidden></div>
<aside id="account-drawer" class="account-drawer" aria-hidden="true">
<header class="drawer-header">
<div>
<strong>Wallets</strong>
<span>Accounts and settings</span>
</div>
<button id="close-account-drawer" type="button" title="Close menu" aria-label="Close menu">×</button>
</header>
<section class="drawer-section">
<span class="drawer-label">Known addresses</span>
<div id="known-addresses" class="known-addresses"></div>
</section>
<nav class="drawer-navigation" aria-label="Wallet menu">
<button id="menu-create-wallet" type="button">
<span class="menu-icon">+</span>
<span><strong>Create another address</strong><small>Generate a new Contractless wallet</small></span>
<span class="menu-arrow"></span>
</button>
<button id="menu-import-wallet" type="button">
<span class="menu-icon"></span>
<span><strong>Import another address</strong><small>Use an image or private key</small></span>
<span class="menu-arrow"></span>
</button>
<button id="menu-settings" type="button">
<span class="menu-icon"></span>
<span><strong>Settings</strong><small>API endpoint and lock timeout</small></span>
<span class="menu-arrow"></span>
</button>
</nav>
<button id="drawer-lock-wallet" class="drawer-lock" type="button">
<span></span>
Lock wallet
</button>
</aside>
<section id="settings-screen" class="screen settings-screen" hidden>
<header class="wallet-header">
<button id="close-settings" class="header-back" type="button" title="Back to assets" aria-label="Back to assets"></button>
<div><strong>Settings</strong><small>Wallet preferences and backup</small></div>
</header>
<div class="settings-content">
<section class="settings-group">
<div class="settings-heading">
<strong>Security</strong>
<span>Control how long the active wallet stays unlocked.</span>
</div>
<label for="settings-lock-timeout">Automatic lock</label>
<select id="settings-lock-timeout">
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="60">60 minutes</option>
<option value="240">4 hours</option>
<option value="720">12 hours</option>
<option value="1440">24 hours</option>
</select>
</section>
<section class="settings-group">
<div class="settings-heading">
<strong>Contractless API</strong>
<span>Choose the API used for lookups and transaction broadcasts.</span>
</div>
<label for="settings-api-url">API URL</label>
<input id="settings-api-url" type="url" value="https://api.contractless.dev" />
</section>
<button id="save-wallet-settings" class="primary settings-save" type="button">Save settings</button>
<section class="settings-group backup-settings">
<div class="settings-heading">
<strong>Active wallet backup</strong>
<span>These actions require the active wallet encryption key.</span>
</div>
<button class="settings-action" data-backup-action="image" type="button">
<span class="settings-action-icon"></span>
<span><strong>Save wallet image</strong><small>Download an encrypted PNG backup</small></span>
<span></span>
</button>
<button class="settings-action" data-backup-action="private" type="button">
<span class="settings-action-icon"></span>
<span><strong>View private key</strong><small>Reveal and copy the active private key</small></span>
<span></span>
</button>
<button class="settings-action" data-backup-action="print" type="button">
<span class="settings-action-icon"></span>
<span><strong>Print wallet backup</strong><small>Private key and encryption key</small></span>
<span></span>
</button>
</section>
<section id="private-key-result" class="private-key-result" hidden>
<div>
<strong>Private key</strong>
<button id="clear-private-key" type="button" title="Clear private key" aria-label="Clear private key">×</button>
</div>
<textarea id="revealed-private-key" readonly></textarea>
<button id="copy-private-key" class="secondary" type="button">Copy private key</button>
</section>
</div>
</section>
<div id="backup-key-backdrop" class="modal-backdrop" hidden></div>
<section id="backup-key-modal" class="backup-key-modal" hidden aria-modal="true" role="dialog" aria-labelledby="backup-key-title">
<header>
<div>
<strong id="backup-key-title">Confirm encryption key</strong>
<span>Required to decrypt this wallet locally.</span>
</div>
<button id="close-backup-key" type="button" title="Cancel" aria-label="Cancel">×</button>
</header>
<div>
<label for="backup-key-input">Encryption key</label>
<div class="input-row">
<input id="backup-key-input" type="password" autocomplete="current-password" />
<button class="reveal" type="button" data-input="backup-key-input">Show</button>
</div>
<button id="confirm-backup-key" class="primary" type="button">Continue</button>
</div>
</section>
<p id="notice" role="alert" aria-live="polite"></p>
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,44 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Contractless Wallet Backup</title>
<script type="module" crossorigin src="/print.js"></script>
<link rel="modulepreload" crossorigin href="/chunks/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="stylesheet" crossorigin href="/assets/print-DTb91-bp.css">
</head>
<body>
<header>
<img src="/assets/contractless-logo-BgfMS-Kf.png" alt="" />
<div>
<h1>Contractless Wallet Backup</h1>
<p>Private recovery information</p>
</div>
</header>
<p class="warning">
Keep this page private. Anyone with the private key and encryption key can
control this wallet.
</p>
<section>
<h2>Wallet</h2>
<pre id="wallet-label"></pre>
</section>
<section>
<h2>Address</h2>
<pre id="wallet-address"></pre>
</section>
<section>
<h2>Private Key</h2>
<pre id="wallet-private-key"></pre>
</section>
<section>
<h2>Encryption Key</h2>
<pre id="wallet-encryption-key"></pre>
</section>
<p id="print-error" class="print-error"></p>
</body>
</html>

View File

@ -1 +0,0 @@
import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";async function a(){const e=new URLSearchParams(location.search).get("id");if(!e)throw new Error("Missing printable wallet backup.");const n=`print-backup:${e}`,o=await chrome.storage.session.get(n);await chrome.storage.session.remove(n);const t=o[n];if(!t)throw new Error("The printable wallet backup has expired.");document.getElementById("wallet-label").textContent=t.label,document.getElementById("wallet-address").textContent=t.address,document.getElementById("wallet-private-key").textContent=t.privateKey,document.getElementById("wallet-encryption-key").textContent=t.encryptionKey,setTimeout(()=>{window.focus(),window.print()},150)}a().catch(e=>{document.getElementById("print-error").textContent=e instanceof Error?e.message:String(e)});

View File

@ -1 +0,0 @@
const a=new Map;window.addEventListener("message",e=>{var s,t,o;if(e.source!==window||((s=e.data)==null?void 0:s.source)!=="contractless-wallet")return;const r=a.get(e.data.id);r&&(a.delete(e.data.id),(t=e.data.response)!=null&&t.ok?r.resolve(e.data.response.result):r.reject(new Error(((o=e.data.response)==null?void 0:o.error)??"Wallet request failed.")))});const n=Object.freeze({isContractless:!0,request({method:e,params:r}){const s=crypto.randomUUID();return new Promise((t,o)=>{a.set(s,{resolve:t,reject:o}),window.postMessage({source:"contractless-page",id:s,method:e,params:r},window.location.origin)})}});Object.defineProperty(window,"contractless",{value:n,writable:!1,configurable:!1});

View File

@ -6,7 +6,7 @@
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"permissions": ["storage", "alarms", "activeTab", "clipboardWrite"],
"permissions": ["storage", "alarms", "clipboardWrite"],
"host_permissions": ["https://api.contractless.dev/*"],
"optional_host_permissions": ["https://*/"],
"action": {

View File

@ -18,7 +18,7 @@
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"permissions": ["storage", "alarms", "activeTab", "clipboardWrite"],
"permissions": ["storage", "alarms", "clipboardWrite"],
"host_permissions": ["https://api.contractless.dev/*"],
"optional_host_permissions": ["https://*/"],
"action": {

View File

@ -120,6 +120,8 @@ let pendingTransfer:
let lastSentTxid = "";
let pendingProviderApprovalId = "";
const CONFIRMED_AFTER_BLOCKS = 20;
const BALANCE_REFRESH_INTERVAL_MS = 15_000;
let balanceLoadInFlight = false;
const TRANSACTION_NAMES: Record<number, string> = {
1: "Mining Reward",
@ -532,12 +534,26 @@ function renderTokenBalances(): void {
});
}
async function loadWalletBalances(): Promise<void> {
if (!walletStatus?.unlocked) return;
walletBalances = undefined;
byId("base-balance").textContent = "--";
byId("base-asset-balance").textContent = "--";
renderTokenBalances();
async function loadWalletBalances(
options: { showLoading?: boolean; reportErrors?: boolean } = {}
): Promise<void> {
if (!walletStatus?.unlocked || balanceLoadInFlight) return;
const requestedAddress = walletStatus.address;
const requestedNetwork = walletStatus.selectedNetwork;
const requestedApiUrl = walletStatus.apiUrl;
const hadBalances = walletBalances !== undefined;
const showLoading = options.showLoading ?? !hadBalances;
const reportErrors = options.reportErrors ?? true;
balanceLoadInFlight = true;
if (showLoading) {
walletBalances = undefined;
byId("base-balance").textContent = "--";
byId("base-asset-balance").textContent = "--";
renderTokenBalances();
}
try {
if (!walletStatus.apiUrl) {
throw new Error(`No ${walletStatus.networkName} API is configured.`);
@ -555,6 +571,16 @@ async function loadWalletBalances(): Promise<void> {
tokenCatalogError?: string;
nftCatalogError?: string;
};
if (
!walletStatus?.unlocked
|| walletStatus.address !== requestedAddress
|| walletStatus.selectedNetwork !== requestedNetwork
|| walletStatus.apiUrl !== requestedApiUrl
) {
return;
}
walletBalances = {
balances: Array.isArray(result.balances) ? result.balances : [],
tokens: Array.isArray(result.tokens) ? result.tokens : [],
@ -572,20 +598,35 @@ async function loadWalletBalances(): Promise<void> {
byId("base-asset-balance").textContent = baseBalance;
renderTokenBalances();
} catch (error) {
const list = byId("token-list");
list.replaceChildren();
const empty = document.createElement("div");
empty.className = "empty-assets balance-error";
const title = document.createElement("strong");
title.textContent = "Unable to load balances";
const description = document.createElement("span");
description.textContent = errorText(error);
empty.append(title, description);
list.append(empty);
notice.textContent = errorText(error);
if (!hadBalances) {
const list = byId("token-list");
list.replaceChildren();
const empty = document.createElement("div");
empty.className = "empty-assets balance-error";
const title = document.createElement("strong");
title.textContent = "Unable to load balances";
const description = document.createElement("span");
description.textContent = errorText(error);
empty.append(title, description);
list.append(empty);
}
if (reportErrors) notice.textContent = errorText(error);
} finally {
balanceLoadInFlight = false;
}
}
function refreshVisibleDashboard(): void {
if (
document.hidden
|| byId("dashboard-screen").hidden
|| !walletStatus?.unlocked
) {
return;
}
void loadWalletBalances({ showLoading: false, reportErrors: false });
}
function nftKey(name: string, series: number): string {
return `${name.trim().toUpperCase()}|${series}`;
}
@ -2285,6 +2326,9 @@ byId("reject-provider-approval").addEventListener("click", () => {
}).catch(() => setApprovalProcessing(null));
});
window.setInterval(refreshVisibleDashboard, BALANCE_REFRESH_INTERVAL_MS);
document.addEventListener("visibilitychange", refreshVisibleDashboard);
send("status")
.then((status) => showStatus(status as WalletStatus))
.catch((error) => {

View File

@ -51,6 +51,32 @@ button:disabled { cursor: not-allowed; }
.screen[hidden] { display: none; }
#create-screen,
#image-screen,
#private-screen,
#verify-screen,
#backup-screen {
overflow-x: hidden;
overflow-y: scroll;
}
#create-screen::-webkit-scrollbar,
#image-screen::-webkit-scrollbar,
#private-screen::-webkit-scrollbar,
#verify-screen::-webkit-scrollbar,
#backup-screen::-webkit-scrollbar {
width: 8px;
}
#create-screen::-webkit-scrollbar-thumb,
#image-screen::-webkit-scrollbar-thumb,
#private-screen::-webkit-scrollbar-thumb,
#verify-screen::-webkit-scrollbar-thumb,
#backup-screen::-webkit-scrollbar-thumb {
border: 2px solid var(--blue);
background: rgb(255 255 255 / 65%);
}
.wallet-header {
height: 72px;
display: flex;