diff --git a/frontend/messages/da.json b/frontend/messages/da.json index 4178071b..7ab5c900 100644 --- a/frontend/messages/da.json +++ b/frontend/messages/da.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary har kun brug for dette til at overvåge dine transaktioner. Dine private nøgler forbliver sikre i din wallet.", - "tryBaconWallet": "Ny her? Prøv med en eksempel-wallet først for at se, hvordan Canary fungerer.", - "useBaconWallet": "Brug Bacon Wallet", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "Ny her? Prøv med en eksempel-wallet først for at se, hvordan Canary fungerer.", + "sampleWallets": { + "bacon": "Brug Bacon Wallet", + "satoshi-genesis": "Brug Satoshi Genesis-adresse" + }, + "guidesPrompt": "Er du i tvivl om, hvordan du finder output descriptors eller XPUB'er? Følg vores vejledninger nedenfor:", "exportSteps": "Følg disse trin for at eksportere din descriptor fra {walletName}", "pasteXpub": "Indsæt din XPUB nedenfor.", "pasteDescriptor": "Indsæt din output descriptor eller XPUB nedenfor.", - "baconPrefilled": "Vi har udfyldt Bacon eksempel-wallet for dig. Klik blot på Tilføj Wallet for at fortsætte.", + "samplePrefilled": "Vi har udfyldt {name} eksempel-wallet for dig. Klik blot på Tilføj Wallet for at fortsætte.", "enterDetails": "Indtast detaljer", "walletType": { "software": "software", diff --git a/frontend/messages/de-DE.json b/frontend/messages/de-DE.json index 9fa82370..61b6b747 100644 --- a/frontend/messages/de-DE.json +++ b/frontend/messages/de-DE.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary braucht dies nur, um Ihre Transaktionen zu überwachen. Ihre privaten Schlüssel bleiben sicher in Ihrer Wallet.", - "tryBaconWallet": "Neu hier? Probieren Sie zuerst eine Beispiel-Wallet aus, um zu sehen, wie Canary funktioniert.", - "useBaconWallet": "Bacon-Wallet verwenden", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "Neu hier? Probieren Sie zuerst eine Beispiel-Wallet aus, um zu sehen, wie Canary funktioniert.", + "sampleWallets": { + "bacon": "Bacon-Wallet verwenden", + "satoshi-genesis": "Satoshi Genesis-Adresse verwenden" + }, + "guidesPrompt": "Unsicher, wie Sie Output-Deskriptoren oder XPUBs erhalten? Folgen Sie unseren Anleitungen unten:", "exportSteps": "Befolgen Sie diese Schritte, um Ihren Descriptor aus {walletName} zu exportieren", "pasteXpub": "Fügen Sie Ihren XPUB unten ein.", "pasteDescriptor": "Fügen Sie Ihren Output-Descriptor oder XPUB unten ein.", - "baconPrefilled": "Wir haben die Bacon-Beispiel-Wallet für Sie vorausgefüllt. Klicken Sie einfach auf Wallet hinzufügen, um fortzufahren.", + "samplePrefilled": "Wir haben die {name}-Beispiel-Wallet für Sie vorausgefüllt. Klicken Sie einfach auf Wallet hinzufügen, um fortzufahren.", "enterDetails": "Details eingeben", "walletType": { "software": "Software", diff --git a/frontend/messages/en-US.json b/frontend/messages/en-US.json index 63b5ff85..2a40b991 100644 --- a/frontend/messages/en-US.json +++ b/frontend/messages/en-US.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary only needs your output descriptor, XPUB, or address to monitor your transactions. Your private keys stay safe in your wallet.", - "tryBaconWallet": "New here? Try with a sample wallet first to see how Canary works.", - "useBaconWallet": "Use Bacon Wallet", + "trySampleWallet": "New here? Try with a sample wallet first to see how Canary works.", + "sampleWallets": { + "bacon": "Use Bacon Wallet", + "satoshi-genesis": "Use Satoshi Genesis Address" + }, "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", "exportSteps": "Follow these steps to export your descriptor from {walletName}", "pasteXpub": "Paste your XPUB below.", "pasteDescriptor": "Paste your output descriptor or XPUB below.", - "baconPrefilled": "We've prefilled the Bacon sample wallet for you. Just click Add Wallet to continue.", + "samplePrefilled": "We've prefilled the {name} sample wallet for you. Just click Add Wallet to continue.", "enterDetails": "Enter Details", "walletType": { "software": "software", diff --git a/frontend/messages/es-419.json b/frontend/messages/es-419.json index a58b7471..764014ed 100644 --- a/frontend/messages/es-419.json +++ b/frontend/messages/es-419.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary solo necesita esto para monitorear tus transacciones. Tus claves privadas permanecen seguras en tu billetera.", - "tryBaconWallet": "¿Nuevo aquí? Prueba primero con una billetera de muestra para ver cómo funciona Canary.", - "useBaconWallet": "Usar Billetera Bacon", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "¿Nuevo aquí? Prueba primero con una billetera de muestra para ver cómo funciona Canary.", + "sampleWallets": { + "bacon": "Usar Billetera Bacon", + "satoshi-genesis": "Usar Dirección Satoshi Genesis" + }, + "guidesPrompt": "¿No estás seguro de cómo obtener descriptores de salida o XPUBs? Sigue nuestras guías a continuación:", "exportSteps": "Sigue estos pasos para exportar tu descriptor desde {walletName}", "pasteXpub": "Pega tu XPUB abajo.", "pasteDescriptor": "Pega tu descriptor de salida o XPUB abajo.", - "baconPrefilled": "Hemos rellenado la billetera de muestra Bacon para ti. Solo haz clic en Agregar Billetera para continuar.", + "samplePrefilled": "Hemos rellenado la billetera de muestra {name} para ti. Solo haz clic en Agregar Billetera para continuar.", "enterDetails": "Ingresar Detalles", "walletType": { "software": "software", diff --git a/frontend/messages/fr-FR.json b/frontend/messages/fr-FR.json index 2b4b59be..59e42e61 100644 --- a/frontend/messages/fr-FR.json +++ b/frontend/messages/fr-FR.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary n'a besoin de cela que pour surveiller vos transactions. Vos clés privées restent en sécurité dans votre portefeuille.", - "tryBaconWallet": "Nouveau ici ? Essayez d'abord avec un portefeuille exemple pour voir comment fonctionne Canary.", - "useBaconWallet": "Utiliser le portefeuille Bacon", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "Nouveau ici ? Essayez d'abord avec un portefeuille exemple pour voir comment fonctionne Canary.", + "sampleWallets": { + "bacon": "Utiliser le portefeuille Bacon", + "satoshi-genesis": "Utiliser l'adresse Satoshi Genesis" + }, + "guidesPrompt": "Vous ne savez pas comment obtenir des descripteurs de sortie ou des XPUBs ? Suivez nos guides ci-dessous :", "exportSteps": "Suivez ces étapes pour exporter votre descripteur depuis {walletName}", "pasteXpub": "Collez votre XPUB ci-dessous.", "pasteDescriptor": "Collez votre descripteur de sortie ou XPUB ci-dessous.", - "baconPrefilled": "Nous avons prérempli le portefeuille exemple Bacon pour vous. Cliquez simplement sur Ajouter le portefeuille pour continuer.", + "samplePrefilled": "Nous avons prérempli le portefeuille exemple {name} pour vous. Cliquez simplement sur Ajouter le portefeuille pour continuer.", "enterDetails": "Entrer les détails", "walletType": { "software": "logiciel", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index c77f2b8c..213a90e0 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canaryはトランザクションの監視にのみ必要です。秘密鍵はウォレット内で安全に保管されます。", - "tryBaconWallet": "初めてですか?まずサンプルウォレットでCanaryの動作を確認してください。", - "useBaconWallet": "Baconウォレットを使用", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "初めてですか?まずサンプルウォレットでCanaryの動作を確認してください。", + "sampleWallets": { + "bacon": "Baconウォレットを使用", + "satoshi-genesis": "Satoshi Genesisアドレスを使用" + }, + "guidesPrompt": "出力ディスクリプタやXPUBの取得方法がわからない場合は、以下のガイドをご覧ください:", "exportSteps": "{walletName}からディスクリプタをエクスポートするには、以下の手順に従ってください", "pasteXpub": "下にXPUBを貼り付けてください。", "pasteDescriptor": "下に出力ディスクリプタまたはXPUBを貼り付けてください。", - "baconPrefilled": "Baconサンプルウォレットを事前入力しました。ウォレットを追加をクリックして続行してください。", + "samplePrefilled": "{name}サンプルウォレットを事前入力しました。ウォレットを追加をクリックして続行してください。", "enterDetails": "詳細を入力", "walletType": { "software": "ソフトウェア", diff --git a/frontend/messages/nb.json b/frontend/messages/nb.json index f450778e..8b769e96 100644 --- a/frontend/messages/nb.json +++ b/frontend/messages/nb.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary trenger bare dette for å overvåke transaksjonene dine. Dine private nøkler forblir trygge i lommeboken din.", - "tryBaconWallet": "Ny her? Prøv med en prøvelommebok først for å se hvordan Canary fungerer.", - "useBaconWallet": "Bruk Bacon-lommebok", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "Ny her? Prøv med en prøvelommebok først for å se hvordan Canary fungerer.", + "sampleWallets": { + "bacon": "Bruk Bacon-lommebok", + "satoshi-genesis": "Bruk Satoshi Genesis-adresse" + }, + "guidesPrompt": "Usikker på hvordan du henter output-deskriptorer eller XPUB-er? Følg veiledningene våre nedenfor:", "exportSteps": "Følg disse trinnene for å eksportere deskriptoren fra {walletName}", "pasteXpub": "Lim inn XPUB nedenfor.", "pasteDescriptor": "Lim inn output-deskriptoren eller XPUB nedenfor.", - "baconPrefilled": "Vi har forhåndsutfylt Bacon-prøvelommeboken for deg. Bare klikk på Legg til lommebok for å fortsette.", + "samplePrefilled": "Vi har forhåndsutfylt {name}-prøvelommeboken for deg. Bare klikk på Legg til lommebok for å fortsette.", "enterDetails": "Skriv inn detaljer", "walletType": { "software": "programvare", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index 0f3c37aa..2b82f410 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "O Canary só precisa disso para monitorar suas transações. Suas chaves privadas permanecem seguras na sua carteira.", - "tryBaconWallet": "Novo aqui? Experimente primeiro com uma carteira de amostra para ver como o Canary funciona.", - "useBaconWallet": "Usar Carteira Bacon", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "Novo aqui? Experimente primeiro com uma carteira de amostra para ver como o Canary funciona.", + "sampleWallets": { + "bacon": "Usar Carteira Bacon", + "satoshi-genesis": "Usar Endereço Satoshi Genesis" + }, + "guidesPrompt": "Não tem certeza de como obter descritores de saída ou XPUBs? Siga nossos guias abaixo:", "exportSteps": "Siga estes passos para exportar seu descritor do {walletName}", "pasteXpub": "Cole seu XPUB abaixo.", "pasteDescriptor": "Cole seu descritor de saída ou XPUB abaixo.", - "baconPrefilled": "Preenchemos a carteira de amostra Bacon para você. Basta clicar em Adicionar Carteira para continuar.", + "samplePrefilled": "Preenchemos a carteira de amostra {name} para você. Basta clicar em Adicionar Carteira para continuar.", "enterDetails": "Inserir Detalhes", "walletType": { "software": "software", diff --git a/frontend/messages/sv.json b/frontend/messages/sv.json index f05d9093..a51536af 100644 --- a/frontend/messages/sv.json +++ b/frontend/messages/sv.json @@ -460,13 +460,16 @@ }, "wizard": { "keysStaySafe": "Canary behöver bara detta för att övervaka dina transaktioner. Dina privata nycklar förblir säkra i din plånbok.", - "tryBaconWallet": "Ny här? Prova med en exempelplånbok först för att se hur Canary fungerar.", - "useBaconWallet": "Använd Bacon Wallet", - "guidesPrompt": "Unsure how to obtain output descriptors or XPUBs? Follow our guides below:", + "trySampleWallet": "Ny här? Prova med en exempelplånbok först för att se hur Canary fungerar.", + "sampleWallets": { + "bacon": "Använd Bacon Wallet", + "satoshi-genesis": "Använd Satoshi Genesis-adress" + }, + "guidesPrompt": "Osäker på hur du hittar output descriptors eller XPUB:ar? Följ våra guider nedan:", "exportSteps": "Följ dessa steg för att exportera din deskriptor från {walletName}", "pasteXpub": "Klistra in din XPUB nedan.", "pasteDescriptor": "Klistra in din utdata-deskriptor eller XPUB nedan.", - "baconPrefilled": "Vi har fyllt i Bacon-exempelplånboken åt dig. Klicka bara på Lägg till plånbok för att fortsätta.", + "samplePrefilled": "Vi har fyllt i {name}-exempelplånboken åt dig. Klicka bara på Lägg till plånbok för att fortsätta.", "enterDetails": "Ange detaljer", "walletType": { "software": "mjukvara", diff --git a/frontend/src/app/wallets/add/[[...slug]]/__tests__/page.test.tsx b/frontend/src/app/wallets/add/[[...slug]]/__tests__/page.test.tsx index 772c05fc..67090da1 100644 --- a/frontend/src/app/wallets/add/[[...slug]]/__tests__/page.test.tsx +++ b/frontend/src/app/wallets/add/[[...slug]]/__tests__/page.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor, act, fireEvent } from '@testing-library/react' import userEvent from '@testing-library/user-event' import AddWalletPage from '../page' -import { SAMPLE_WALLET_SLUG } from '@/components/add-wallet-form' +import { SAMPLE_WALLETS } from '@/components/add-wallet-form' // Import ApiError from utils to use in tests import { ApiError } from '../../../../../lib/utils' @@ -142,7 +142,7 @@ describe('AddWalletPage', () => { it('shows form with prefilled data for bacon wallet', async () => { await act(async () => { - renderWithSlug([SAMPLE_WALLET_SLUG]) + renderWithSlug([SAMPLE_WALLETS[0].slug]) }) await waitFor(() => { @@ -176,7 +176,7 @@ describe('AddWalletPage', () => { } }) - it('shows Bacon wallet option for first wallet', async () => { + it('shows sample wallet options for first wallet', async () => { walletsContextMockValue = { ...defaultWalletsContextMock, wallets: [] } await act(async () => { @@ -187,10 +187,11 @@ describe('AddWalletPage', () => { expect(screen.getByText('Use Bacon Wallet')).toBeInTheDocument() }) + expect(screen.getByText('Use Satoshi Genesis Address')).toBeInTheDocument() expect(screen.getByText(/Try with a sample wallet/)).toBeInTheDocument() }) - it('hides Bacon wallet option when wallets exist', async () => { + it('hides sample wallet options when non-sample wallets exist', async () => { walletsContextMockValue = { ...defaultWalletsContextMock, wallets: [{ checksum: 'test', name: 'Test Wallet' }] as never[], @@ -205,6 +206,47 @@ describe('AddWalletPage', () => { }) expect(screen.queryByText('Use Bacon Wallet')).not.toBeInTheDocument() + expect(screen.queryByText('Use Satoshi Genesis Address')).not.toBeInTheDocument() + }) + + it('still shows remaining sample wallets when one sample wallet is added', async () => { + walletsContextMockValue = { + ...defaultWalletsContextMock, + wallets: [{ checksum: 'bacon123', name: 'Bacon' }] as never[], + } + + await act(async () => { + renderWithSlug(undefined) + }) + + await waitFor(() => { + expect(screen.getByText('Use Satoshi Genesis Address')).toBeInTheDocument() + }) + + // Bacon should not be shown since it's already added + expect(screen.queryByText('Use Bacon Wallet')).not.toBeInTheDocument() + }) + + it('hides sample prompt when all sample wallets are added', async () => { + walletsContextMockValue = { + ...defaultWalletsContextMock, + wallets: [ + { checksum: 'bacon123', name: 'Bacon' }, + { checksum: 'satoshi123', name: 'Satoshi (Genesis)' }, + ] as never[], + } + + await act(async () => { + renderWithSlug(undefined) + }) + + await waitFor(() => { + expect(screen.getByText('Sparrow')).toBeInTheDocument() + }) + + expect(screen.queryByText(/Try with a sample wallet/)).not.toBeInTheDocument() + expect(screen.queryByText('Use Bacon Wallet')).not.toBeInTheDocument() + expect(screen.queryByText('Use Satoshi Genesis Address')).not.toBeInTheDocument() }) }) @@ -223,7 +265,7 @@ describe('AddWalletPage', () => { } }) - it('hides Bacon wallet option in cloud mode', async () => { + it('hides sample wallet options in cloud mode', async () => { walletsContextMockValue = { ...defaultWalletsContextMock, wallets: [] } await act(async () => { @@ -235,6 +277,7 @@ describe('AddWalletPage', () => { }) expect(screen.queryByText('Use Bacon Wallet')).not.toBeInTheDocument() + expect(screen.queryByText('Use Satoshi Genesis Address')).not.toBeInTheDocument() }) it('shows upgrade prompt when wallet limit reached', async () => { @@ -285,7 +328,7 @@ describe('AddWalletPage', () => { expect(screen.getByText('Sparrow')).toBeInTheDocument() }) - it('navigates to bacon form when Bacon wallet is clicked', async () => { + it('fills form inline when Bacon wallet is clicked', async () => { const user = userEvent.setup() walletsContextMockValue = { ...defaultWalletsContextMock, wallets: [] } @@ -299,7 +342,34 @@ describe('AddWalletPage', () => { await user.click(screen.getByText('Use Bacon Wallet')) - expect(mockPush).toHaveBeenCalledWith(`/wallets/add/${SAMPLE_WALLET_SLUG}`) + // Form should be filled inline, no navigation + expect(mockPush).not.toHaveBeenCalled() + const nameInput = screen.getByLabelText('Wallet Name') as HTMLInputElement + await waitFor(() => { + expect(nameInput.value).toBe('Bacon') + }) + }) + + it('fills form inline when Satoshi Genesis is clicked', async () => { + const user = userEvent.setup() + walletsContextMockValue = { ...defaultWalletsContextMock, wallets: [] } + + await act(async () => { + renderWithSlug(undefined) + }) + + await waitFor(() => { + expect(screen.getByText('Use Satoshi Genesis Address')).toBeInTheDocument() + }) + + await user.click(screen.getByText('Use Satoshi Genesis Address')) + + // Form should be filled inline, no navigation + expect(mockPush).not.toHaveBeenCalled() + const nameInput = screen.getByLabelText('Wallet Name') as HTMLInputElement + await waitFor(() => { + expect(nameInput.value).toBe('Satoshi (Genesis)') + }) }) }) diff --git a/frontend/src/app/wallets/add/[[...slug]]/page.tsx b/frontend/src/app/wallets/add/[[...slug]]/page.tsx index 376e1362..b83149da 100644 --- a/frontend/src/app/wallets/add/[[...slug]]/page.tsx +++ b/frontend/src/app/wallets/add/[[...slug]]/page.tsx @@ -8,6 +8,7 @@ import { useAuth } from "@/contexts/auth-context" import { useWalletsContext } from "@/contexts/wallets-context" import { api } from "@/lib/api" import { hasReachedWalletLimit } from "@/lib/utils" +import { SAMPLE_WALLETS } from "@/components/add-wallet-form" import { useBlockHeader } from "@/hooks/useBlockHeader" import { useWalletWizard } from "@/hooks/useWalletWizard" import { Wallet } from "@/types" @@ -39,11 +40,10 @@ function AddWalletPageContent({ slug }: { slug?: string[] }) { const { step, selectedWallet, - isBaconWallet, - baconWallet, + isSampleWallet, + sampleWallet, handleNavigateToChoose, handleSelectWallet, - handleSelectSampleWallet, getGuideSteps, } = useWalletWizard({ slug, network, t: t as unknown as { raw: (key: string) => unknown } }) @@ -94,6 +94,9 @@ function AddWalletPageContent({ slug }: { slug?: string[] }) { } const isFirstWallet = walletCount === 0 + const sampleWalletNames = SAMPLE_WALLETS.map(sw => sw.name) + const hasOnlySampleWallets = wallets.every(w => sampleWalletNames.includes(w.name)) + const showSampleWallets = isSelfHostedMode && hasOnlySampleWallets const hasPaidSubscription = billingStatus?.subscription_status === 'active' && !!billingStatus?.stripe_customer_id // Loading state @@ -146,11 +149,12 @@ function AddWalletPageContent({ slug }: { slug?: string[] }) { selectedWallet={selectedWallet} step={step} onNavigateToChoose={handleNavigateToChoose} - isSelfHostedMode={isSelfHostedMode} + showSampleWallets={showSampleWallets} isFirstWallet={isFirstWallet} onSelectWallet={handleSelectWallet} - onSelectSampleWallet={handleSelectSampleWallet} onWalletCreated={handleWalletCreated} + wallets={wallets} + network={network} t={t} tNav={tNav} /> @@ -179,9 +183,9 @@ function AddWalletPageContent({ slug }: { slug?: string[] }) { selectedWallet={selectedWallet} step={step} onNavigateToChoose={handleNavigateToChoose} - isBaconWallet={isBaconWallet} + isSampleWallet={isSampleWallet} isFirstWallet={isFirstWallet} - baconWallet={baconWallet} + sampleWallet={sampleWallet} onWalletCreated={handleWalletCreated} t={t} tNav={tNav} diff --git a/frontend/src/components/add-wallet-form.tsx b/frontend/src/components/add-wallet-form.tsx index ec8feb32..94d1e5ce 100644 --- a/frontend/src/components/add-wallet-form.tsx +++ b/frontend/src/components/add-wallet-form.tsx @@ -18,23 +18,51 @@ import { Wallet } from "@/types" import { XPUB_REGEX, DESCRIPTOR_REGEX, isValidBitcoinAddress, getDescriptorScriptType } from "@/lib/constants" import { useTranslations } from "next-intl" -// Slug for the sample wallet route -export const SAMPLE_WALLET_SLUG = 'bacon' +type NetworkKey = 'mainnet' | 'testnet' | 'regtest' -// Well-known "bacon" test wallet (12x "bacon" as BIP39 mnemonic) -export const SAMPLE_WALLETS: Record<'mainnet' | 'testnet' | 'regtest', { name: string; descriptor: string }> = { - mainnet: { +export interface SampleWallet { + slug: string + name: string + descriptor: string // Default descriptor (mainnet, or network-agnostic for raw pubkeys) + networkOverrides?: Partial> +} + +// Satoshi's genesis block coinbase public key (uncompressed, network-agnostic) +const GENESIS_PUBKEY = "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f" + +export const SAMPLE_WALLETS: SampleWallet[] = [ + { + slug: 'bacon', name: "Bacon", descriptor: "wpkh([00000000/84h/0h/0h]xpub6DEzNop46vmxR49zYWFnMwmEfawSNmAMf6dLH5YKDY463twtvw1XD7ihwJRLPRGZJz799VPFzXHpZu6WdhT29WnaeuChS6aZHZPFmqczR5K/<0;1>/*)#4jhrljfg", + networkOverrides: { + testnet: { descriptor: "wpkh([9a6a2580/84h/1h/0h]tpubDCMRAYcH71Gagskm7E5peNMYB5sKaLLwtn2c4Rb3CMUTRVUk5dkpsskhspa5MEcVZ11LwTcM7R5mzndUCG9WabYcT5hfQHbYVoaLFBZHPCi/<0;1>/*)#4laqdwct" }, + regtest: { descriptor: "wpkh([9a6a2580/84h/1h/0h]tpubDCMRAYcH71Gagskm7E5peNMYB5sKaLLwtn2c4Rb3CMUTRVUk5dkpsskhspa5MEcVZ11LwTcM7R5mzndUCG9WabYcT5hfQHbYVoaLFBZHPCi/<0;1>/*)#4laqdwct" }, + }, }, - testnet: { - name: "Bacon", - descriptor: "wpkh([9a6a2580/84h/1h/0h]tpubDCMRAYcH71Gagskm7E5peNMYB5sKaLLwtn2c4Rb3CMUTRVUk5dkpsskhspa5MEcVZ11LwTcM7R5mzndUCG9WabYcT5hfQHbYVoaLFBZHPCi/<0;1>/*)#4laqdwct", - }, - regtest: { - name: "Bacon", - descriptor: "wpkh([9a6a2580/84h/1h/0h]tpubDCMRAYcH71Gagskm7E5peNMYB5sKaLLwtn2c4Rb3CMUTRVUk5dkpsskhspa5MEcVZ11LwTcM7R5mzndUCG9WabYcT5hfQHbYVoaLFBZHPCi/<0;1>/*)#4laqdwct", + { + slug: 'satoshi-genesis', + name: "Satoshi (Genesis)", + descriptor: GENESIS_PUBKEY, + networkOverrides: { + regtest: { descriptor: "bcrt1q20lu6ldqtssq7y7ewarlamlzldnmyk5w4n3e97" }, + }, + // testnet: uses default descriptor (same raw pubkey, network-agnostic, will be empty) }, +] + +export function isSampleWalletSlug(slug: string): boolean { + return SAMPLE_WALLETS.some(w => w.slug === slug) +} + +export function getSampleWalletForNetwork(slug: string, network: string): { name: string; descriptor: string } | undefined { + const wallet = SAMPLE_WALLETS.find(w => w.slug === slug) + if (!wallet) return undefined + const override = wallet.networkOverrides?.[network as NetworkKey] + return { + name: wallet.name, + descriptor: override?.descriptor ?? wallet.descriptor, + } } interface AddWalletFormProps { diff --git a/frontend/src/components/wallet-wizard/wallet-form-step.tsx b/frontend/src/components/wallet-wizard/wallet-form-step.tsx index e25914e8..4f485288 100644 --- a/frontend/src/components/wallet-wizard/wallet-form-step.tsx +++ b/frontend/src/components/wallet-wizard/wallet-form-step.tsx @@ -10,9 +10,9 @@ interface WalletFormStepProps { selectedWallet: WalletGuide | null step: WizardStep onNavigateToChoose: () => void - isBaconWallet: boolean + isSampleWallet: boolean isFirstWallet: boolean - baconWallet: { name: string; descriptor: string } + sampleWallet: { name: string; descriptor: string } | undefined onWalletCreated: (wallet: Wallet) => void t: (key: string, params?: Record) => string tNav: (key: string) => string @@ -22,9 +22,9 @@ export function WalletFormStep({ selectedWallet, step, onNavigateToChoose, - isBaconWallet, + isSampleWallet, isFirstWallet, - baconWallet, + sampleWallet, onWalletCreated, t, tNav, @@ -42,8 +42,8 @@ export function WalletFormStep({

- {isBaconWallet - ? t('add.wizard.baconPrefilled') + {isSampleWallet && sampleWallet + ? t('add.wizard.samplePrefilled', { name: sampleWallet.name }) : (selectedWallet?.outputType === 'xpub' ? t('add.wizard.pasteXpub') : t('add.wizard.pasteDescriptor')) }

@@ -55,8 +55,8 @@ export function WalletFormStep({ isFirstWallet={isFirstWallet} onWalletCreated={onWalletCreated} autoFocusDescriptor={false} - initialName={isBaconWallet ? baconWallet.name : undefined} - initialDescriptor={isBaconWallet ? baconWallet.descriptor : undefined} + initialName={isSampleWallet && sampleWallet ? sampleWallet.name : undefined} + initialDescriptor={isSampleWallet && sampleWallet ? sampleWallet.descriptor : undefined} outputType={selectedWallet?.outputType} /> diff --git a/frontend/src/components/wallet-wizard/wallet-type-selector.tsx b/frontend/src/components/wallet-wizard/wallet-type-selector.tsx index 7020eb32..65e7b217 100644 --- a/frontend/src/components/wallet-wizard/wallet-type-selector.tsx +++ b/frontend/src/components/wallet-wizard/wallet-type-selector.tsx @@ -1,11 +1,12 @@ "use client" +import { useState } from "react" import Image from "next/image" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Lightbulb } from "lucide-react" import { walletGuides, type WalletGuide } from "@/lib/wallet-guides" -import { AddWalletForm } from "@/components/add-wallet-form" +import { AddWalletForm, SAMPLE_WALLETS, getSampleWalletForNetwork } from "@/components/add-wallet-form" import { Wallet } from "@/types" import { WizardBreadcrumb, WizardStep } from "./wizard-breadcrumb" @@ -13,11 +14,12 @@ interface WalletTypeSelectorProps { selectedWallet: WalletGuide | null step: WizardStep onNavigateToChoose: () => void - isSelfHostedMode: boolean + showSampleWallets: boolean isFirstWallet: boolean onSelectWallet: (wallet: WalletGuide) => void - onSelectSampleWallet: () => void onWalletCreated: (wallet: Wallet) => void + wallets: Wallet[] + network: string t: (key: string, params?: Record) => string tNav: (key: string) => string } @@ -26,14 +28,26 @@ export function WalletTypeSelector({ selectedWallet, step, onNavigateToChoose, - isSelfHostedMode, + showSampleWallets, isFirstWallet, onSelectWallet, - onSelectSampleWallet, onWalletCreated, + wallets, + network, t, tNav, }: WalletTypeSelectorProps) { + const [selectedSampleSlug, setSelectedSampleSlug] = useState(null) + + // Filter out sample wallets that have already been added + const addedWalletNames = new Set(wallets.map(w => w.name)) + const availableSampleWallets = SAMPLE_WALLETS.filter(sw => !addedWalletNames.has(sw.name)) + + // Resolve selected sample wallet for current network + const selectedSample = selectedSampleSlug + ? getSampleWalletForNetwork(selectedSampleSlug, network) + : undefined + return (
- {/* Bacon sample wallet for self-hosted first wallet */} - {isSelfHostedMode && isFirstWallet && ( -
- -

- {t('add.wizard.tryBaconWallet')} -

- + {/* Sample wallets prompt */} + {showSampleWallets && availableSampleWallets.length > 0 && ( +
+ +
+

+ {selectedSample + ? t('add.wizard.samplePrefilled', { name: selectedSample.name }) + : t('add.wizard.trySampleWallet') + } +

+
+ {availableSampleWallets.map((wallet) => ( + + ))} +
+
)} @@ -75,6 +101,8 @@ export function WalletTypeSelector({ diff --git a/frontend/src/hooks/useWalletWizard.ts b/frontend/src/hooks/useWalletWizard.ts index 9ec8729a..2d40df13 100644 --- a/frontend/src/hooks/useWalletWizard.ts +++ b/frontend/src/hooks/useWalletWizard.ts @@ -1,7 +1,7 @@ import { useMemo, useEffect, useCallback } from "react" import { useRouter } from "next/navigation" import { walletGuides, type WalletGuide } from "@/lib/wallet-guides" -import { SAMPLE_WALLET_SLUG, SAMPLE_WALLETS } from "@/components/add-wallet-form" +import { isSampleWalletSlug, getSampleWalletForNetwork } from "@/components/add-wallet-form" import type { WizardStep } from "@/components/wallet-wizard/wizard-breadcrumb" interface UseWalletWizardOptions { @@ -13,35 +13,36 @@ interface UseWalletWizardOptions { interface UseWalletWizardReturn { step: WizardStep selectedWallet: WalletGuide | null - isBaconWallet: boolean - baconWallet: { name: string; descriptor: string } + isSampleWallet: boolean + sampleWallet: { name: string; descriptor: string } | undefined handleNavigateToChoose: () => void handleSelectWallet: (wallet: WalletGuide) => void - handleSkipToForm: () => void - handleSelectSampleWallet: () => void getGuideSteps: (walletId: string) => string[] } export function useWalletWizard({ slug, network, t }: UseWalletWizardOptions): UseWalletWizardReturn { const router = useRouter() - // Get bacon wallet for current network - const baconWallet = SAMPLE_WALLETS[network as keyof typeof SAMPLE_WALLETS] || SAMPLE_WALLETS.mainnet + // Check if we're using any sample wallet + const isSampleWallet = slug?.[0] != null && isSampleWalletSlug(slug[0]) - // Check if we're using the Bacon sample wallet - const isBaconWallet = slug?.[0] === SAMPLE_WALLET_SLUG + // Get sample wallet data for current network + const sampleWallet = useMemo(() => { + if (!isSampleWallet || !slug?.[0]) return undefined + return getSampleWalletForNetwork(slug[0], network) + }, [isSampleWallet, slug, network]) // Derive selected wallet from URL const selectedWallet = useMemo(() => { if (!slug || slug.length === 0) return null - if (slug[0] === 'form' || slug[0] === SAMPLE_WALLET_SLUG) return null + if (slug[0] === 'form' || isSampleWalletSlug(slug[0])) return null return walletGuides.find(w => w.id === slug[0]) || null }, [slug]) // Derive current step from URL const step: WizardStep = useMemo(() => { if (!slug || slug.length === 0) return 'choose' - if (slug[0] === 'form' || slug[0] === SAMPLE_WALLET_SLUG) return 'form' + if (slug[0] === 'form' || isSampleWalletSlug(slug[0])) return 'form' if (selectedWallet) return 'instructions' return 'choose' }, [slug, selectedWallet]) @@ -49,7 +50,7 @@ export function useWalletWizard({ slug, network, t }: UseWalletWizardOptions): U // Redirect invalid wallet IDs to clean choose URL // Also redirect legacy /wallets/add/{wallet-id}/form URLs to /wallets/add/{wallet-id} useEffect(() => { - if (slug && slug.length > 0 && slug[0] !== 'form' && slug[0] !== SAMPLE_WALLET_SLUG && !selectedWallet) { + if (slug && slug.length > 0 && slug[0] !== 'form' && !isSampleWalletSlug(slug[0]) && !selectedWallet) { router.replace('/wallets/add') } // Redirect legacy /wallets/add/{wallet-id}/form to /wallets/add/{wallet-id} @@ -67,14 +68,6 @@ export function useWalletWizard({ slug, network, t }: UseWalletWizardOptions): U router.push(`/wallets/add/${wallet.id}`) }, [router]) - const handleSkipToForm = useCallback(() => { - router.push('/wallets/add/form') - }, [router]) - - const handleSelectSampleWallet = useCallback(() => { - router.push(`/wallets/add/${SAMPLE_WALLET_SLUG}`) - }, [router]) - // Helper to get translated guide steps const getGuideSteps = useCallback((walletId: string): string[] => { try { @@ -92,12 +85,10 @@ export function useWalletWizard({ slug, network, t }: UseWalletWizardOptions): U return { step, selectedWallet, - isBaconWallet, - baconWallet, + isSampleWallet, + sampleWallet, handleNavigateToChoose, handleSelectWallet, - handleSkipToForm, - handleSelectSampleWallet, getGuideSteps, } } diff --git a/scripts/dev.sh b/scripts/dev.sh index 3a739909..d5926b45 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -1537,6 +1537,54 @@ case "$1" in echo " ✅ Bacon already funded" fi + # Create Satoshi (Genesis) wallet (deterministic single-address - for sample wallet onboarding) + # Mimics prod where the raw Satoshi pubkey creates a single-address pk() wallet + SATOSHI_GENESIS_TPRV="tprv8ZgxMBicQKsPeZjnkSokuUQsdrWJ83bXz4Eqm1aVDkDSSJ9BqHGMsjxpBEb3n6V9X3u6ThQQ1dmsvigtXWxvP8YJL9FST4DighMqnHtmFTo" + # Deterministic first address derived from this tprv (BIP84 path 84h/1h/0h/0/0) + SATOSHI_GENESIS_ADDRESS="bcrt1q20lu6ldqtssq7y7ewarlamlzldnmyk5w4n3e97" + echo "📋 Creating Satoshi (Genesis) wallet..." + btc unloadwallet "satoshi-genesis" 2>/dev/null || true + + set +e + CREATE_RESULT=$(btc -named createwallet wallet_name="satoshi-genesis" disable_private_keys=false blank=true passphrase="" avoid_reuse=false descriptors=true 2>&1) + CREATE_EXIT_CODE=$? + set -e + + if echo "$CREATE_RESULT" | grep -q "already exists"; then + echo " ✅ Satoshi (Genesis) wallet exists, loading..." + btc loadwallet "satoshi-genesis" >/dev/null 2>&1 || true + elif [ $CREATE_EXIT_CODE -eq 0 ]; then + echo " ✅ Satoshi (Genesis) blank wallet created" + + # Import deterministic descriptors (needed to own the address for funding) + SATOSHI_EXT_RAW="wpkh($SATOSHI_GENESIS_TPRV/84h/1h/0h/0/*)" + SATOSHI_INT_RAW="wpkh($SATOSHI_GENESIS_TPRV/84h/1h/0h/1/*)" + SATOSHI_EXT_CHECKSUM=$(btc getdescriptorinfo "$SATOSHI_EXT_RAW" | jq -r '.checksum') + SATOSHI_INT_CHECKSUM=$(btc getdescriptorinfo "$SATOSHI_INT_RAW" | jq -r '.checksum') + + btc_wallet "satoshi-genesis" importdescriptors "[ + {\"desc\": \"${SATOSHI_EXT_RAW}#${SATOSHI_EXT_CHECKSUM}\", \"timestamp\": \"now\", \"active\": true, \"internal\": false, \"range\": [0, 999]}, + {\"desc\": \"${SATOSHI_INT_RAW}#${SATOSHI_INT_CHECKSUM}\", \"timestamp\": \"now\", \"active\": true, \"internal\": true, \"range\": [0, 999]} + ]" >/dev/null 2>&1 + echo " ✅ Satoshi (Genesis) wallet seeded with deterministic descriptors" + else + echo " ❌ Failed to create Satoshi (Genesis) wallet: $CREATE_RESULT" + exit 1 + fi + + # Fund Satoshi (Genesis) wallet at its deterministic first address + echo "💰 Funding Satoshi (Genesis) wallet..." + SATOSHI_GENESIS_BALANCE=$(btc_wallet "satoshi-genesis" getbalance 2>/dev/null || echo "0") + + if [ "$(echo "$SATOSHI_GENESIS_BALANCE == 0" | bc -l 2>/dev/null || echo "1")" -eq 1 ]; then + echo " 💸 Sending 0.5 BTC to Satoshi (Genesis) address..." + btc_miner sendtoaddress "$SATOSHI_GENESIS_ADDRESS" 0.5 >/dev/null 2>&1 + btc generatetoaddress 1 "$MINER_ADDRESS" >/dev/null 2>&1 + echo " ✅ Satoshi (Genesis) funded with 0.5 BTC at $SATOSHI_GENESIS_ADDRESS" + else + echo " ✅ Satoshi (Genesis) already funded" + fi + # Create single-address wallets (one per address type, for testing address monitoring) echo "📋 Creating single-address wallets..." ADDR_TYPES=("legacy" "p2sh-segwit" "bech32" "bech32m") @@ -1591,8 +1639,9 @@ case "$1" in echo " taproot-empty (tr): $TAPROOT_EMPTY_DESCRIPTOR" echo "" echo "📱 Other wallets:" - echo " 🎭 Charlie (funded - 0.5 BTC at index 250): $CHARLIE_DESCRIPTOR" - echo " 🥓 Bacon (demo - ~0.08 BTC): $BACON_DESCRIPTOR" + echo " 🎭 Charlie (funded - 0.5 BTC at index 250): $CHARLIE_DESCRIPTOR" + echo " 🥓 Bacon (demo - ~0.08 BTC): $BACON_DESCRIPTOR" + echo " 🪙 Satoshi Genesis (sample - 0.5 BTC): $SATOSHI_GENESIS_ADDRESS" echo "" echo "📍 Single addresses (for address monitoring):" for i in "${!ADDR_WALLET_NAMES[@]}"; do @@ -1718,6 +1767,7 @@ case "$1" in btc unloadwallet "taproot-empty" 2>/dev/null || true btc unloadwallet "charlie" 2>/dev/null || true btc unloadwallet "bacon" 2>/dev/null || true + btc unloadwallet "satoshi-genesis" 2>/dev/null || true btc unloadwallet "miner" 2>/dev/null || true fi