{/* Telegram uses its icon on phones and in translated headers.
Status chips take turns here to leave room for the menu. */}
-
+
{queued ? : }
@@ -148,7 +148,7 @@ export function SiteHeader() {
-
+
diff --git a/apps/web/src/lib/wallet/wallet-context.tsx b/apps/web/src/lib/wallet/wallet-context.tsx
index 2006fd4..796dad8 100644
--- a/apps/web/src/lib/wallet/wallet-context.tsx
+++ b/apps/web/src/lib/wallet/wallet-context.tsx
@@ -7,6 +7,7 @@ import {
WalletProvider as SdkWalletProvider,
useWallet as useSdkWallet,
} from "@xcp/wallet-sdk/react";
+import { CASATOOKAN_WALLET, discoverWallets, KNOWN_WALLETS } from "@xcp/wallet-sdk/web";
import "@/lib/wallet/sdk-config";
export type { ProofStatus };
@@ -28,9 +29,15 @@ const STATUS: Record = {
reload_required: "disconnected",
};
+const wallets = discoverWallets(
+ process.env.NEXT_PUBLIC_CASATOOKAN_ENABLED === "true"
+ ? [...KNOWN_WALLETS, CASATOOKAN_WALLET]
+ : KNOWN_WALLETS,
+);
+
export function WalletProvider({ children }: { children: ReactNode }) {
// Connect is login here: a wallet that proves nothing at connect (Horizon) is asked to sign the proof.
- return {children};
+ return {children};
}
export function useWallet() {
diff --git a/apps/web/src/locales/_source.json b/apps/web/src/locales/_source.json
index 6d8fdb2..8a7020a 100644
--- a/apps/web/src/locales/_source.json
+++ b/apps/web/src/locales/_source.json
@@ -1355,6 +1355,9 @@
"components/connect-button.tsx",
"components/header-wallet.tsx"
],
+ "Connect a wallet that supports this inscription reveal to finish the launch.": [
+ "app/[lang]/create/page.tsx"
+ ],
"Connect a wallet to chat.": [
"components/chat-panel.tsx"
],
@@ -3866,6 +3869,9 @@
"Upload token image": [
"app/[lang]/create/page.tsx"
],
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": [
+ "app/[lang]/create/page.tsx"
+ ],
"Use at most {max} lines.": [
"components/chat-panel.tsx"
],
diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json
index a3d00b4..034407c 100644
--- a/apps/web/src/locales/es.json
+++ b/apps/web/src/locales/es.json
@@ -1298,5 +1298,7 @@
"No expiry": "Sin vencimiento",
"Burned": "Quemado",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "Incluye los tokens enviados a la dirección de quema y los tokens destruidos explícitamente.",
- "Destroyed": "Destruidos"
+ "Destroyed": "Destruidos",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "Usa una billetera compatible con inscripciones de imágenes o lanza con una imagen alojada.",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "Conecta una billetera compatible con la revelación de esta inscripción para finalizar el lanzamiento."
}
diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json
index 39b6e95..8080d79 100644
--- a/apps/web/src/locales/fr.json
+++ b/apps/web/src/locales/fr.json
@@ -1298,5 +1298,7 @@
"No expiry": "Sans expiration",
"Burned": "Brûlés",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "Comprend les tokens envoyés à l’adresse de destruction et les tokens explicitement détruits.",
- "Destroyed": "Détruits"
+ "Destroyed": "Détruits",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "Utilisez un portefeuille compatible avec les inscriptions d’images, ou lancez avec une image hébergée.",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "Connectez un portefeuille compatible avec la révélation de cette inscription pour terminer le lancement."
}
diff --git a/apps/web/src/locales/ja.json b/apps/web/src/locales/ja.json
index 6477d9d..9b37845 100644
--- a/apps/web/src/locales/ja.json
+++ b/apps/web/src/locales/ja.json
@@ -1298,5 +1298,7 @@
"No expiry": "有効期限なし",
"Burned": "バーン済み",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "バーンアドレスに送信されたトークンと、明示的に破棄されたトークンを含みます。",
- "Destroyed": "破棄済み"
+ "Destroyed": "破棄済み",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "画像のインスクリプションに対応したウォレットを使うか、ホストされた画像でローンチしてください。",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "このインスクリプションのリビールに対応したウォレットを接続して、ローンチを完了してください。"
}
diff --git a/apps/web/src/locales/ko.json b/apps/web/src/locales/ko.json
index 5374039..d1abcd2 100644
--- a/apps/web/src/locales/ko.json
+++ b/apps/web/src/locales/ko.json
@@ -1298,5 +1298,7 @@
"No expiry": "만료 없음",
"Burned": "소각됨",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "소각 주소로 전송된 토큰과 명시적으로 파기된 토큰을 포함합니다.",
- "Destroyed": "파기됨"
+ "Destroyed": "파기됨",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "이미지 인스크립션을 지원하는 지갑을 사용하거나 호스팅된 이미지로 출시하세요.",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "출시를 완료하려면 이 인스크립션의 리빌을 지원하는 지갑을 연결하세요."
}
diff --git a/apps/web/src/locales/pt.json b/apps/web/src/locales/pt.json
index e414123..abe1bd8 100644
--- a/apps/web/src/locales/pt.json
+++ b/apps/web/src/locales/pt.json
@@ -1298,5 +1298,7 @@
"No expiry": "Sem expiração",
"Burned": "Queimados",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "Inclui tokens enviados ao endereço de queima e tokens explicitamente destruídos.",
- "Destroyed": "Destruídos"
+ "Destroyed": "Destruídos",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "Use uma carteira compatível com inscrições de imagens ou lance com uma imagem hospedada.",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "Conecte uma carteira compatível com a revelação desta inscrição para concluir o lançamento."
}
diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json
index 3f56063..aabe80e 100644
--- a/apps/web/src/locales/ru.json
+++ b/apps/web/src/locales/ru.json
@@ -1298,5 +1298,7 @@
"No expiry": "Бессрочно",
"Burned": "Сожжено",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "Включает токены, отправленные на адрес сжигания, и токены, уничтоженные явно.",
- "Destroyed": "Уничтожено"
+ "Destroyed": "Уничтожено",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "Используйте кошелёк с поддержкой записи изображений в блокчейн или запустите токен с изображением на сервере.",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "Подключите кошелёк с поддержкой раскрытия этой записи, чтобы завершить запуск."
}
diff --git a/apps/web/src/locales/uk.json b/apps/web/src/locales/uk.json
index fb5577e..2fdfe3d 100644
--- a/apps/web/src/locales/uk.json
+++ b/apps/web/src/locales/uk.json
@@ -1298,5 +1298,7 @@
"No expiry": "Безстроково",
"Burned": "Спалено",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "Включає токени, надіслані на адресу спалювання, і токени, знищені явно.",
- "Destroyed": "Знищено"
+ "Destroyed": "Знищено",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "Використовуйте гаманець із підтримкою запису зображень у блокчейн або запустіть токен із зображенням на сервері.",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "Підключіть гаманець із підтримкою розкриття цього запису, щоб завершити запуск."
}
diff --git a/apps/web/src/locales/zh-hk.json b/apps/web/src/locales/zh-hk.json
index 2e6ef82..a378487 100644
--- a/apps/web/src/locales/zh-hk.json
+++ b/apps/web/src/locales/zh-hk.json
@@ -1298,5 +1298,7 @@
"No expiry": "永不過期",
"Burned": "已銷毀",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "包括傳送到銷毀地址的代幣及透過銷毀交易銷毀的代幣。",
- "Destroyed": "已銷毀"
+ "Destroyed": "已銷毀",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "請使用支援圖片銘文的錢包,或使用託管圖片發行。",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "請連接支援此銘文揭示交易的錢包以完成發行。"
}
diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json
index 188c616..0baded2 100644
--- a/apps/web/src/locales/zh-tw.json
+++ b/apps/web/src/locales/zh-tw.json
@@ -1298,5 +1298,7 @@
"No expiry": "永不過期",
"Burned": "已銷毀",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "包括傳送到銷毀地址的代幣和透過銷毀交易銷毀的代幣。",
- "Destroyed": "已銷毀"
+ "Destroyed": "已銷毀",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "請使用支援圖片銘文的錢包,或使用託管圖片發行。",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "請連接支援此銘文揭示交易的錢包以完成發行。"
}
diff --git a/apps/web/src/locales/zh.json b/apps/web/src/locales/zh.json
index 8c16fc9..8f3ba6d 100644
--- a/apps/web/src/locales/zh.json
+++ b/apps/web/src/locales/zh.json
@@ -1298,5 +1298,7 @@
"No expiry": "永不过期",
"Burned": "已销毁",
"Includes tokens sent to the burn address and explicitly destroyed tokens.": "包括发送到销毁地址的代币和通过销毁交易销毁的代币。",
- "Destroyed": "已销毁"
+ "Destroyed": "已销毁",
+ "Use a wallet that supports image inscriptions, or launch with a hosted image.": "请使用支持图片铭文的钱包,或使用托管图片发行。",
+ "Connect a wallet that supports this inscription reveal to finish the launch.": "请连接支持此铭文揭示交易的钱包以完成发行。"
}
diff --git a/browser-tests/casatookan/connect.spec.mjs b/browser-tests/casatookan/connect.spec.mjs
new file mode 100644
index 0000000..69611ca
--- /dev/null
+++ b/browser-tests/casatookan/connect.spec.mjs
@@ -0,0 +1,221 @@
+import { expect, test } from '@playwright/test'
+import { secp256k1 } from '@noble/curves/secp256k1'
+import { legacyMessageHash } from '@xcp/wallet-sdk'
+
+const route = '/es/profile'
+const enabled = process.env.CASATOOKAN_TEST_ENABLED !== 'false'
+const address = 'bc1qvux25709r4uw6rzc8wyl7wwecjdhrx085hm5ty'
+const publicKey = '0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967'
+const taproot = 'bc1pfqarxerx6v0zh6zcj2f3a3np8jselxn8vgefdx95c0n328qpzg4ssw6kv5'
+const unsupported = '3N722gUQ2YmphkTzm9nsgD8rkxh1xvukYC'
+const header = page => page.getByRole('banner')
+const connect = page => header(page).getByRole('button', { name: 'Conectar', exact: true }).first()
+const account = page => header(page).getByText(/bc1q.*5ty/).filter({ visible: true }).first()
+
+async function bridge(page, options = {}) {
+ await page.exposeFunction('signCasaProof', message => {
+ const signature = secp256k1.sign(legacyMessageHash(message), new Uint8Array(32).fill(9))
+ return Buffer.from([31 + signature.recovery, ...signature.toCompactRawBytes()]).toString('base64')
+ })
+ await page.addInitScript(({ address, publicKey, taproot, unsupported, options }) => {
+ const listeners = new Map()
+ const state = {
+ requests: 0, signatures: 0, psbts: 0, reject: false, unsupported: false,
+ granted: sessionStorage.getItem('casa-grant') === 'yes',
+ revoke() {
+ state.granted = false
+ sessionStorage.removeItem('casa-grant')
+ for (const fn of listeners.get('accountsChanged') ?? []) fn([])
+ },
+ inject() {
+ Object.assign(window, {
+ ...(options.marker === false ? {} : { casaTookan: { isCasaTookan: true } }),
+ unisat: {
+ requestAccounts: async () => {
+ state.requests++
+ if (state.reject) throw { code: 4001, message: 'User rejected connection' }
+ state.granted = true
+ sessionStorage.setItem('casa-grant', 'yes')
+ return [state.unsupported ? unsupported : options.taproot ? taproot : address]
+ },
+ getAccounts: async () => state.granted ? [state.unsupported ? unsupported : options.taproot ? taproot : address] : [],
+ getPublicKey: async () => publicKey,
+ getNetwork: async () => 'livenet',
+ signMessage: async message => { state.signatures++; return window.signCasaProof(message) },
+ signPsbt: async () => { state.psbts++; throw new Error('Unexpected transaction signing') },
+ on: (event, fn) => {
+ if (!listeners.has(event)) listeners.set(event, new Set())
+ listeners.get(event).add(fn)
+ },
+ removeListener: (event, fn) => listeners.get(event)?.delete(fn),
+ },
+ })
+ },
+ }
+ window.casaTest = state
+ if (!options.delay) state.inject()
+ if (options.competing) window.HorizonWalletProvider = { request: async () => { throw new Error('Wrong wallet selected') } }
+ }, { address, publicKey, taproot, unsupported, options })
+ for (const host of ['api.xcp.io', 'api.xcp.fun', 'mempool.space', 'api.digirare.com']) {
+ await page.route('https://' + host + '/**', route => route.fulfill({ status: 503, json: { error: 'API disabled in wallet connection test' } }))
+ }
+}
+
+async function disconnect(page) {
+ if (await page.getByRole('button', { name: 'Desconectar', exact: true }).count() === 0) await account(page).click()
+ await page.getByRole('button', { name: 'Desconectar', exact: true }).click()
+}
+
+test('pilot remains absent when disabled', async ({ page }) => {
+ test.skip(enabled)
+ await bridge(page)
+ await page.goto(route)
+ await connect(page).click()
+ await expect(page.getByText('Casa Tookan', { exact: true })).toHaveCount(0)
+ expect(await page.evaluate(() => window.casaTest.requests)).toBe(0)
+})
+
+test.describe('enabled pilot', () => {
+ test.skip(!enabled)
+
+ test('connect verifies the proof, restores silently, and disconnects', async ({ page }) => {
+ await bridge(page)
+ await page.goto(route)
+ await connect(page).click()
+ await expect(account(page)).toBeVisible()
+ await expect.poll(() => page.evaluate(() => window.casaTest.signatures)).toBe(1)
+
+ await page.reload()
+ await expect(account(page)).toBeVisible()
+ expect(await page.evaluate(() => window.casaTest.requests)).toBe(0)
+ await disconnect(page)
+ await expect(connect(page)).toBeVisible()
+ await page.reload()
+ await expect(connect(page)).toBeVisible()
+ expect(await page.evaluate(() => window.casaTest.psbts)).toBe(0)
+ })
+
+ test('rejection and unsupported accounts show an error and permit retry', async ({ page }) => {
+ await bridge(page)
+ await page.goto(route)
+ await page.evaluate(() => { window.casaTest.reject = true })
+ await connect(page).click()
+ await expect(header(page).getByRole('alert')).toBeVisible()
+ await page.evaluate(() => { window.casaTest.reject = false; window.casaTest.unsupported = true })
+ await connect(page).click()
+ await expect(header(page).getByRole('alert')).toContainText('Legacy, Native SegWit or BIP-86 Taproot')
+ await page.evaluate(() => { window.casaTest.unsupported = false })
+ await connect(page).click()
+ await expect(account(page)).toBeVisible()
+ await expect(header(page).getByRole('alert')).toHaveCount(0)
+ })
+
+ test('Taproot connects with its declared proof dialect', async ({ page }) => {
+ await bridge(page, { taproot: true })
+ await page.goto(route)
+ await connect(page).click()
+ await expect(page.getByText(/bc1p.*kv5/).filter({ visible: true }).first()).toBeVisible()
+ })
+
+ test('revocation clears both live and restored connections', async ({ page }) => {
+ await bridge(page)
+ await page.goto(route)
+ await connect(page).click()
+ await expect(account(page)).toBeVisible()
+ await page.evaluate(() => window.casaTest.revoke())
+ await expect(connect(page)).toBeVisible()
+ await connect(page).click()
+ await expect(account(page)).toBeVisible()
+ await page.evaluate(() => sessionStorage.removeItem('casa-grant'))
+ await page.reload()
+ await expect(connect(page)).toBeVisible()
+ expect(await page.evaluate(() => window.casaTest.requests)).toBe(0)
+ })
+
+ test('chooser selects Casa when another wallet is installed', async ({ page }) => {
+ await bridge(page, { competing: true })
+ await page.goto(route)
+ await connect(page).click()
+ await page.getByRole('listitem').filter({ hasText: 'Casa Tookan' }).getByRole('button', { name: 'Conectar', exact: true }).click()
+ await expect(account(page)).toBeVisible()
+ })
+
+ test('mobile connect discovers a late bridge without an install detour', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 })
+ await bridge(page, { delay: true })
+ await page.goto(route)
+ await expect(connect(page)).toBeVisible()
+ await page.evaluate(() => window.casaTest.inject())
+ await connect(page).click()
+ await expect(account(page)).toBeVisible()
+ await expect(page.getByText('Solo en computadora, por ahora', { exact: true })).toHaveCount(0)
+ expect(await header(page).evaluate(el => el.scrollWidth <= window.innerWidth)).toBe(true)
+ await page.screenshot({ path: test.info().outputPath('mobile-connected.png') })
+ await disconnect(page)
+ await expect(connect(page)).toBeVisible()
+ })
+
+ test('Unisat alone is not detected as Casa', async ({ page }) => {
+ await bridge(page, { marker: false })
+ await page.goto(route)
+ await connect(page).click()
+ expect(await page.evaluate(() => window.casaTest.requests)).toBe(0)
+ await expect(page.getByRole('listitem').filter({ hasText: 'Casa Tookan' }).getByRole('button', { name: 'Conectar', exact: true })).toHaveCount(0)
+ })
+})
+
+test('app metadata and missing paths preserve JSON/404 and CORS', async ({ request }) => {
+ const manifest = await request.get('/.well-known/wallet-app.json')
+ expect(manifest.status()).toBe(200)
+ expect(manifest.headers()['access-control-allow-origin']).toBe('*')
+ expect((await manifest.json()).wallet.version).toBe('1')
+ const missing = await request.get('/.well-known/missing-casa-test.json')
+ expect(missing.status()).toBe(404)
+ expect(missing.headers()['access-control-allow-origin']).toBe('*')
+})
+
+for (const width of [1280, 390]) {
+ test(`Taproot inscription is unavailable before upload or signing at ${width}px`, async ({ page }) => {
+ test.skip(!enabled)
+ await page.setViewportSize({ width, height: 900 })
+ await bridge(page, { taproot: true })
+ let uploads = 0
+ await page.route('**/api/launches', route => {
+ if (route.request().method() === 'POST') uploads++
+ return route.fulfill({ status: 503, json: {} })
+ })
+ await page.goto('/es/create')
+ await connect(page).click()
+ await expect(page.getByText(/bc1p.*kv5/).filter({ visible: true }).first()).toBeVisible()
+ await expect(page.getByRole('checkbox')).toBeDisabled()
+ await expect(page.getByText('Usa una billetera compatible con inscripciones de imágenes o lanza con una imagen alojada.')).toBeVisible()
+ await expect(page.locator('input[type=file]')).toBeAttached()
+ expect(uploads).toBe(0)
+ expect(await page.evaluate(() => window.casaTest.psbts)).toBe(0)
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
+ })
+}
+
+for (const signed of [false, true]) {
+ test(`keeps pending inscription and allows resume only for signed reveal (${signed})`, async ({ page }) => {
+ test.skip(!enabled)
+ await bridge(page, { taproot: true })
+ const pending = {
+ version: 1, address: taproot, asset: 'TESTLAUNCH', startBlock: 999999,
+ revealScript: '51', tapInternalKey: '11'.repeat(32), commitTxid: '22'.repeat(32),
+ commitVout: 0, commitAmount: 10000, commitRawTx: '00', recipient: taproot,
+ feeRate: 1, createdAt: Date.now(), ...(signed ? { revealRawTx: '00' } : {}),
+ }
+ await page.addInitScript(record => localStorage.setItem('xcp.fun:pending-reveal:' + record.address, JSON.stringify(record)), pending)
+ await page.goto('/es/create')
+ await connect(page).click()
+ const finish = page.getByRole('button', { name: /TESTLAUNCH/ })
+ if (signed) await expect(finish).toBeEnabled()
+ else {
+ await expect(finish).toBeDisabled()
+ await expect(page.getByText('Conecta una billetera compatible con la revelación de esta inscripción para finalizar el lanzamiento.')).toBeVisible()
+ }
+ expect(await page.evaluate(address => JSON.parse(localStorage.getItem('xcp.fun:pending-reveal:' + address)), taproot)).toEqual(pending)
+ expect(await page.evaluate(() => window.casaTest.psbts)).toBe(0)
+ })
+}
diff --git a/docs/casatookan.md b/docs/casatookan.md
new file mode 100644
index 0000000..ecf018e
--- /dev/null
+++ b/docs/casatookan.md
@@ -0,0 +1,61 @@
+# Casa Tookan app
+
+The app manifest is served at `/.well-known/wallet-app.json`. The PNGs under `/wallet-app/` provide its icon, banner and splash. Paths are root-relative so they resolve outside `/.well-known/`.
+
+The manifest makes this site discoverable when a user saves it in Casa Tookan. It does not register the site in a curated directory or enable new signing methods. Bitcoin support is optional because browsing needs no wallet. The splash uses Casa Tookan's automatic readiness timeout.
+
+Wallet connection support is an opt-in pilot; see below.
+
+## Deployment check
+
+- Fetch the manifest without cookies and confirm JSON, HTTP 200 and `Access-Control-Allow-Origin: *`.
+- Fetch a missing `/.well-known/` path and a missing `/wallet-app/` image: each must return 404 with CORS, not the homepage.
+- Fetch all three images: 1024×1024 opaque icon, 1200×630 banner, 200×200 splash.
+- Open the site in the native Casa Tookan browser, save it and reopen its app card. Check the title, artwork and splash dismissal.
+
+Headers are configured in both Next (SSR/404) and Cloudflare Static Assets (public files). Production framing and CSP policies are unchanged; the development iframe bridge is not installed.
+
+Format: https://docs.casatookan.com/apps.html
+
+## Wallet connection pilot
+
+Build with `NEXT_PUBLIC_CASATOOKAN_ENABLED=true` to include Casa Tookan in the SDK chooser. It is off by default. The SDK is pinned to `089ca36869ab72440292c3064143e7dc0ea8e75c` (XCP/wallet-sdk#13), including revoked-grant recovery and session-listener restart fixes.
+
+Mainnet Legacy, Native SegWit and BIP-86 Taproot accounts are supported. Ordinary compose flows use the SDK PSBT fallback. The adapter supports selected inputs, foreign signatures, non-ALL authorizations and one approval for up to 20 PSBTs. Every PSBT is limited to 64 KiB, with explicit prevouts. Nested SegWit, multisig and script-path commit/reveal remain unsupported. Casa does not validate application intents; existing site validation remains required.
+
+`npm run test:casatookan` exercises the actual site connection UI with a deterministic test bridge, including signed proofs where the site requests them, rejection/retry, unsupported accounts, chooser selection, mobile late injection, disconnect and revoked grants after reload. `CASATOOKAN_TEST_ENABLED=false npm run test:casatookan` checks the disabled pilot (set the environment variable in the syntax for your shell). Tests use an unfunded fixture and never broadcast.
+
+The SDK has separate live web-development-signer evidence in its PR. These site regressions are simulated-provider tests, not installed iOS/Android or funded-trade verification. Those checks remain required before enabling production.
+
+## Local QA — 2026-10-20
+
+Seven enabled browser checks and two disabled-pilot checks passed on `/es/profile`. The desktop and mobile header connects, signs the proof, restores, rejects unsupported accounts and disconnects. Web typecheck, ESLint, numeric checks and all locale checks passed; 26 session/signature tests passed. All lockfiles pass `npm ci --dry-run --ignore-scripts`.
+
+The browser fixture signs deterministic test messages. Public API calls are blocked, so these runs do not prove live backend authentication or transaction composition. No production deployment or broadcast was performed.
+
+An earlier local development run self-redirected `/profile`. Production checks subsequently returned `/profile` 200 (English), `/en/profile` 308 to `/profile`, and `/es/profile` 200 (Spanish). No production routing defect is established and this PR does not change the locale proxy.
+
+## Expanded SDK support
+
+Pinned SDK `089ca36869ab72440292c3064143e7dc0ea8e75c` passed 481 unit tests and a real dev-wallet check of Taproot proof signing, a shared PSBT v2 transaction, a reusable authorization and a twenty-transaction batch. Evidence is in XCP/wallet-sdk#13. These are unfunded signing checks; installed-phone lifecycle and funded settlement remain unverified.
+
+Expanded site browser suite: 8 passing checks, including Taproot connection, restore/revocation and mobile discovery. The browser suite uses a deterministic injected bridge; separate live-wallet evidence is recorded in the SDK PR.
+
+Web typecheck, ESLint, numeric checks and all locale checks pass; the pinned lockfile passes an npm ci dry run.
+
+## Inscription boundary
+
+The real Casa signer rejects both script-path variants and tree-bearing key-path
+recovery. The create page requires the wallet's `commit-and-reveal` capability
+before allowing a new inscription, and rechecks before uploading metadata.
+Casa users can launch normally with a hosted image. A selected inscription is
+not silently converted into a hosted launch on wallet change.
+
+Pending launches remain stored. An unsigned reveal requires a capable wallet;
+an already-signed reveal can still be resumed without asking Casa to sign it.
+
+Inscription follow-up validation: 34 safety/recovery unit tests and four new
+browser tests pass against the updated SDK. Browser coverage includes desktop
+and mobile restrictions before upload/signing, retained unsigned reveals, and
+resuming already-signed reveals. Web typecheck, ESLint, numeric checks, all ten
+locale checks, and the lockfile dry run pass.
diff --git a/package-lock.json b/package-lock.json
index 788374c..3fb55ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,8 @@
"packages/*"
],
"devDependencies": {
+ "@noble/curves": "1.9.7",
+ "@playwright/test": "1.63.0",
"@scure/bip32": "^2.3.0",
"esbuild": "0.28.2",
"happy-dom": "20.14.0",
@@ -42,7 +44,7 @@
"@noble/hashes": "^1.8.0",
"@scure/base": "^2.2.0",
"@scure/btc-signer": "^2.2.0",
- "@xcp/wallet-sdk": "github:XCP/wallet-sdk#5a056ce83dc3a999f9384a7cb625094471d19c91",
+ "@xcp/wallet-sdk": "github:XCP/wallet-sdk#089ca36869ab72440292c3064143e7dc0ea8e75c",
"babel-plugin-react-compiler": "^1.0.0",
"bignumber.js": "^9.3.1",
"next": "16.3.4",
@@ -3923,6 +3925,23 @@
"url": "https://github.com/sponsors/Boshen"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
+ "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "playwright": "1.63.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@poppinss/colors": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
@@ -7809,8 +7828,7 @@
},
"node_modules/@xcp/wallet-sdk": {
"version": "0.5.0",
- "resolved": "git+ssh://git@github.com/XCP/wallet-sdk.git#5a056ce83dc3a999f9384a7cb625094471d19c91",
- "integrity": "sha512-lDrfqg+AcJYaPt1QDEiYbMYVJRMIKoCvxnI5CB9/UNHGH21EiNzvh8X7vBgsdP+5JWAKVybl4jUOk+enOcuGBQ==",
+ "resolved": "git+ssh://git@github.com/XCP/wallet-sdk.git#089ca36869ab72440292c3064143e7dc0ea8e75c",
"license": "MIT",
"dependencies": {
"@noble/curves": "^1.9.7",
@@ -13285,6 +13303,35 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/playwright": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
+ "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.63.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
+ "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
diff --git a/package.json b/package.json
index cdde981..95da419 100644
--- a/package.json
+++ b/package.json
@@ -18,9 +18,12 @@
"deploy:web": "npm run deploy --workspace apps/web",
"deploy:api": "npm run deploy --workspace apps/api && npm run smoke",
"migrate:api:local": "npm run migrate:local --workspace apps/api",
- "migrate:api:remote": "npm run migrate:remote --workspace apps/api"
+ "migrate:api:remote": "npm run migrate:remote --workspace apps/api",
+ "test:casatookan": "playwright test -c playwright.casatookan.config.mjs"
},
"devDependencies": {
+ "@noble/curves": "1.9.7",
+ "@playwright/test": "1.63.0",
"@scure/bip32": "^2.3.0",
"esbuild": "0.28.2",
"happy-dom": "20.14.0",
diff --git a/playwright.casatookan.config.mjs b/playwright.casatookan.config.mjs
new file mode 100644
index 0000000..d8e164f
--- /dev/null
+++ b/playwright.casatookan.config.mjs
@@ -0,0 +1,14 @@
+import { defineConfig } from '@playwright/test'
+
+export default defineConfig({
+ testDir: './browser-tests/casatookan',
+ timeout: 60_000,
+ workers: 1,
+ use: { baseURL: 'http://127.0.0.1:3411', trace: 'retain-on-failure', hasTouch: true },
+ webServer: {
+ command: 'npm run dev --workspace apps/web -- --hostname 127.0.0.1 --port 3411',
+ url: 'http://127.0.0.1:3411/.well-known/wallet-app.json',
+ timeout: 180_000,
+ env: { NEXT_PUBLIC_CASATOOKAN_ENABLED: process.env.CASATOOKAN_TEST_ENABLED ?? 'true' },
+ },
+})