Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,40 @@ configureToolbox({
url: process.env.EXPO_PUBLIC_TEST_HANDLE_REGISTRY_URL ?? ''
}
},
// Per-chain service overrides. Anything left unset uses the toolbox's
// built-in default for that chain (its NETWORKS table): each chain has its
// own Arcade, chaintracks and MessageBox server. Teratest has no TAAL ARC,
// so it takes no TAAL key; scaletest has no WhatsOnChain, so it takes no
// WoC key either.
services: {
main: {
arcUrl: process.env.EXPO_PUBLIC_ARC_URL,
arcApiKey: process.env.EXPO_PUBLIC_ARC_API_KEY,
chaintracksUrl: process.env.EXPO_PUBLIC_CHAINTRACKS_URL,
messageBoxUrl: process.env.EXPO_PUBLIC_DEFAULT_MESSAGEBOX_URL,
whatsOnChainApiKey: process.env.EXPO_PUBLIC_WOC_API_KEY,
taalApiKey: process.env.EXPO_PUBLIC_WOC_API_KEY
},
test: {
arcUrl: process.env.EXPO_PUBLIC_TEST_ARC_URL,
arcApiKey: process.env.EXPO_PUBLIC_TEST_ARC_API_KEY,
chaintracksUrl: process.env.EXPO_PUBLIC_TEST_CHAINTRACKS_URL,
messageBoxUrl: process.env.EXPO_PUBLIC_TEST_MESSAGEBOX_URL,
whatsOnChainApiKey: process.env.EXPO_PUBLIC_TEST_WOC_API_KEY,
taalApiKey: process.env.EXPO_PUBLIC_TEST_TAAL_API_KEY
},
teratest: {
arcUrl: process.env.EXPO_PUBLIC_TERATEST_ARC_URL,
arcApiKey: process.env.EXPO_PUBLIC_TERATEST_ARC_API_KEY,
chaintracksUrl: process.env.EXPO_PUBLIC_TERATEST_CHAINTRACKS_URL,
whatsOnChainApiKey: process.env.EXPO_PUBLIC_TERATEST_WOC_API_KEY,
taalApiKey: process.env.EXPO_PUBLIC_TERATEST_WOC_API_KEY
messageBoxUrl: process.env.EXPO_PUBLIC_TERATEST_MESSAGEBOX_URL,
whatsOnChainApiKey: process.env.EXPO_PUBLIC_TERATEST_WOC_API_KEY
},
scaletest: {
arcUrl: process.env.EXPO_PUBLIC_SCALETEST_ARC_URL,
arcApiKey: process.env.EXPO_PUBLIC_SCALETEST_ARC_API_KEY,
chaintracksUrl: process.env.EXPO_PUBLIC_SCALETEST_CHAINTRACKS_URL,
messageBoxUrl: process.env.EXPO_PUBLIC_SCALETEST_MESSAGEBOX_URL
}
},
// Payment push notifications (MessageBox device registration). The adapter
Expand Down
60 changes: 60 additions & 0 deletions packages/expo-wallet-toolbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
# Changelog

## Unreleased

### Breaking

- `AppChain` gains `'scaletest'` (the scaling teratestnet; toolbox chain
`'regtest'`), and `WalletChain` gains `'regtest'`. `toWalletChain` now
returns the toolbox's own `Chain` type. Exhaustive `Record<AppChain, …>`
maps in a host need an entry for it.
- `wocConfigFor(network)` returns `undefined` for a network with no
WhatsOnChain (scaletest). `makeBeefRepair` accepts that and never repairs.
- `createWocBroadcastService(woc, apiKey?)` takes the network's WhatsOnChain
endpoints (`NETWORKS[chain].woc`) instead of a chain string.
- `makePeerPayClient` requires `network`. `useMessageBoxConfig(t, network)` and
`useOfflineNoticeActions({ …, network })` take the open network.
- `KNOWN_ARC_URLS` is per network (`Record<AppChain, KnownArcUrl[]>`): test
chains no longer offer mainnet ARC presets.

### Networks

- `NETWORKS` (`core/networks.ts`) is the one per-network table: Arcade,
MessageBox, WhatsOnChain/explorer, TAAL/GorillaPool/Bitails fallbacks,
overlay preset, push. The ternaries that sent any unnamed network to
teratest's endpoints are gone.
- **Each network has its own MessageBox server**: `messagebox-testnet`,
`messagebox-ttn` and `messagebox-tstn` `.bsvblockchain.tech` beside mainnet's
`messagebox.bsvblockchain.tech`. A saved preference equal to any network's
default (or the retired one) follows the open network's default; a server the
user chose is kept. Hosts can override a chain's default with
`services.<chain>.messageBoxUrl`.
- MessageBox / PeerPay clients now look recipients up in the open network's
overlay (`bindMessageBoxNetwork`). `PeerPayClient` 2.5.3 does not pass
`networkPreset` through, so every network had been asking mainnet's overlay.
A network with no overlay routes only to the configured host.
- Identity lookups use the open network's overlay (teratest had been using
testnet's).
- Push registration happens only for profiles on a network whose MessageBox
sends push (mainnet). A test-chain profile's existing registration at
mainnet's box is skipped, not withdrawn.
- Upgrading moves a testnet/teratest profile on the default server to its own
network's box: anything still unread in mainnet's box for that profile is no
longer read. Queued outbox retries keep the host recorded per entry.
- Settings › MessageBox stores a network default (any network's) as "no
preference", so the screen shows the host the rails actually use.
- Bitails is no longer registered as a broadcast / proof fallback on teratest.
Teratest no longer takes a TAAL key.
- **Fix:** a teratest wallet's storage reports chain `'ttn'`, but
`storageMatchesNetwork` expected `'test'`, so the home screen always treated
a teratest wallet as the previous network's. It now compares the toolbox
chain.
- Scaling teratestnet (`scaletest`) is wired through but **not available** in
the network picker (`NETWORKS.scaletest.available = false`). The deployed
chain runs Teranode regtest parameters (regtest genesis, bits `0x207fffff`),
so its toolbox chain is `'regtest'`, not `'tstn'`, and its header window is
anchored at the regtest genesis. The installed
`@bsv/wallet-toolbox-mobile` 2.14.x has no `'regtest'` chain and rejects
every header above the mainnet proof-of-work limit; `regtest` support lands
in 2.15.0 (bsv-blockchain/ts-stack#819). With no WhatsOnChain it also has no
address rail, sweep, BEEF repair or explorer link; Pay shows the address
option as unavailable.

## 0.15.2

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const otherLanguages = Object.keys(resources).filter(code => code !== 'en')
// names, loanwords a language actually uses, and pure format strings.
const allowedUntranslated: Record<string, string[]> = {
teratest: ['*'],
scaletest: ['*'],
mainnet: ['*'],
testnet: ['*'],
settings_version: ['*'],
Expand Down
19 changes: 14 additions & 5 deletions packages/expo-wallet-toolbox/__tests__/net/chainMatch.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import { storageChainFor, storageMatchesNetwork } from '../../core/net/chainMatch'

it('maps both testnets onto the one storage chain', () => {
// Storage records the TOOLBOX's chain name (it is built with
// createStorageBaseOptions(walletChain)), so that is what each network expects.
it('maps each network onto the toolbox chain its storage records', () => {
expect(storageChainFor('main')).toBe('main')
expect(storageChainFor('test')).toBe('test')
// 'teratest' is 'ttn' to the toolbox but still a testnet database.
expect(storageChainFor('teratest')).toBe('test')
expect(storageChainFor('teratest')).toBe('ttn')
expect(storageChainFor('scaletest')).toBe('regtest')
})

it('matches storage to the network it belongs to', () => {
expect(storageMatchesNetwork({ chain: 'main' }, 'main')).toBe(true)
expect(storageMatchesNetwork({ chain: 'test' }, 'teratest')).toBe(true)
// A teratest wallet's storage says 'ttn'. Expecting 'test' here kept every
// teratest home screen treating its own wallet as the previous network's.
expect(storageMatchesNetwork({ chain: 'ttn' }, 'teratest')).toBe(true)
expect(storageMatchesNetwork({ chain: 'regtest' }, 'scaletest')).toBe(true)
})

// The whole point: during a switch the old chain's storage is still mounted,
// and reading it is what showed a mainnet balance under a testnet label.
it('refuses the previous chain while a switch is in flight', () => {
expect(storageMatchesNetwork({ chain: 'main' }, 'test')).toBe(false)
expect(storageMatchesNetwork({ chain: 'test' }, 'main')).toBe(false)
expect(storageMatchesNetwork({ chain: 'test' }, 'teratest')).toBe(false)
expect(storageMatchesNetwork({ chain: 'ttn' }, 'test')).toBe(false)
expect(storageMatchesNetwork({ chain: 'ttn' }, 'scaletest')).toBe(false)
})

it('treats no storage as no match rather than as permission to read', () => {
it('treats no storage, or an unknown network, as no match rather than as permission to read', () => {
expect(storageMatchesNetwork(null, 'main')).toBe(false)
expect(storageMatchesNetwork(undefined, 'main')).toBe(false)
expect(storageMatchesNetwork({}, 'main')).toBe(false)
expect(storageMatchesNetwork({ chain: 'test' }, 'garbage')).toBe(false)
})
78 changes: 78 additions & 0 deletions packages/expo-wallet-toolbox/__tests__/networks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { APP_CHAINS, NETWORKS, hasOverlay, isAppChain } from '../core/networks'
import { toWalletChain } from '../core/config'
import { HEADER_CHECKPOINTS } from '../core/headers/checkpoints'
import { createServices } from '../core/services/walletServiceConfig'
import { configureToolbox, resetToolboxConfig } from '../core/toolboxConfig'

describe('NETWORKS', () => {
it('maps every app chain to its toolbox chain', () => {
expect(APP_CHAINS.map(toWalletChain)).toEqual(['main', 'test', 'ttn', 'regtest'])
})

it('gives every network its own Arcade and MessageBox server, over https', () => {
const arcades = APP_CHAINS.map(c => NETWORKS[c].arcadeUrl)
const boxes = APP_CHAINS.map(c => NETWORKS[c].messageBoxUrl)
expect(new Set(arcades).size).toBe(APP_CHAINS.length)
expect(new Set(boxes).size).toBe(APP_CHAINS.length)
for (const url of [...arcades, ...boxes]) expect(url).toMatch(/^https:\/\/[^/]+$/)
})

it('has WhatsOnChain everywhere except the scaling teratestnet', () => {
expect(NETWORKS.teratest.woc?.apiBase).toBe('https://api.woc-ttn.bsvblockchain.tech')
expect(NETWORKS.scaletest.woc).toBeUndefined()
})

it('looks identities up in each network’s own overlay, and in none on scaletest', () => {
expect(APP_CHAINS.map(c => NETWORKS[c].overlayPreset)).toEqual(['mainnet', 'testnet', 'teratestnet', 'local'])
expect(hasOverlay('teratest')).toBe(true)
expect(hasOverlay('scaletest')).toBe(false)
})

it('registers push only with the server that sends it', () => {
expect(APP_CHAINS.filter(c => NETWORKS[c].push)).toEqual(['main'])
})

it('only knows the app chain names', () => {
expect(isAppChain('scaletest')).toBe(true)
expect(isAppChain('ttn')).toBe(false)
expect(isAppChain(undefined)).toBe(false)
})
})

// The deployed scaling teratestnet runs regtest parameters, which the installed
// toolbox (2.14.x) cannot verify. Until @bsv/wallet-toolbox 2.15.0 adds the
// 'regtest' chain it must stay out of the picker.
describe('scaletest is wired but not offered', () => {
afterEach(() => resetToolboxConfig())

it('is not available, while every other network is', () => {
expect(APP_CHAINS.filter(c => !NETWORKS[c].available)).toEqual(['scaletest'])
})

it('anchors its header window at the regtest genesis, which every chain reset keeps', () => {
expect(HEADER_CHECKPOINTS.regtest).toEqual({
height: 0,
hash: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206'
})
})

// A canary. The installed toolbox has no 'regtest' chain, which is why
// scaletest is gated off. When this starts failing, the dependency supports
// regtest (@bsv/wallet-toolbox >= 2.15.0, bsv-blockchain/ts-stack#819): set
// NETWORKS.scaletest.available, drop the casts in toWalletChain and
// headerStore, and replace this test with one that builds the services.
it('cannot build its services on the installed toolbox yet', () => {
configureToolbox({ backupUrl: null })
const stub = { getChain: async () => 'regtest' } as never
expect(() =>
createServices(
'scaletest',
'callback-token',
{ timestamp: new Date(), base: 'USD', rate: 1 },
undefined,
undefined,
stub
)
).toThrow()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,23 @@ describe('derivation key material', () => {

describe('wocConfigFor', () => {
it('maps mainnet', () => {
expect(wocConfigFor('main')).toEqual({
expect(wocConfigFor('main')!).toEqual({
apiBase: 'https://api.whatsonchain.com',
segment: 'main',
network: 'mainnet'
})
})

it('maps testnet', () => {
expect(wocConfigFor('test')).toEqual({
expect(wocConfigFor('test')!).toEqual({
apiBase: 'https://api.whatsonchain.com',
segment: 'test',
network: 'testnet'
})
})

it('maps teratest to its own WoC host', () => {
expect(wocConfigFor('teratest')).toEqual({
expect(wocConfigFor('teratest')!).toEqual({
apiBase: 'https://api.woc-ttn.bsvblockchain.tech',
segment: 'test',
network: 'testnet'
Expand Down
12 changes: 8 additions & 4 deletions packages/expo-wallet-toolbox/__tests__/pay/addressRail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
TO_ADDRESS_LABEL_PREFIX
} from '../../core/pay/counterparty'

const woc = wocConfigFor('main')
const woc = wocConfigFor('main')!
const ADDRESS = '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'

// A coinbase-shaped spend: one input with an empty unlocking script, one
Expand Down Expand Up @@ -667,16 +667,20 @@ describe('WhatsOnChain API key', () => {
}
})
for (const chain of ['main', 'test', 'teratest'] as const) {
const config = wocConfigFor(chain)
const config = wocConfigFor(chain)!
expect(config.apiKey).toBeUndefined()
expect('apiKey' in config).toBe(false)
expect(JSON.stringify(config)).not.toContain('-key')
}
})

it('answers undefined for a network with no WhatsOnChain', () => {
expect(wocConfigFor('scaletest')).toBeUndefined()
})

it('does not throw before the toolbox is configured', () => {
expect(() => wocConfigFor('main')).not.toThrow()
expect(wocConfigFor('main').apiKey).toBeUndefined()
expect(() => wocConfigFor('main')!).not.toThrow()
expect(wocConfigFor('main')!.apiKey).toBeUndefined()
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Beef, MerklePath, P2PKH, PrivateKey, Transaction, Utils } from '@bsv/sd
import { makeBeefRepair, refetchAtomicBeef, type FetchLike } from '../../core/pay/beefRepair'
import { wocConfigFor } from '../../core/pay/rails/address'

const woc = wocConfigFor('main')
const woc = wocConfigFor('main')!

/** A payment whose parent carries a proof at `height`. */
function paymentAt(height: number): { txid: string; beefHex: string; parentTxid: string } {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import { MessageBoxClient, PeerPayClient } from '@bsv/message-box-client'
import { LookupResolver, PrivateKey, ProtoWallet, type WalletInterface } from '@bsv/sdk'
import {
bindMessageBoxNetwork,
DEFAULT_MESSAGE_BOX_URL,
defaultMessageBoxUrlFor,
LEGACY_MESSAGE_BOX_URL,
makePeerPayClient,
NO_MESSAGE_BOX,
readMessageBoxUrl,
resolveMessageBoxUrl
} from '../../core/pay/rails/handle'
import { configureToolbox, resetToolboxConfig } from '../../core/toolboxConfig'

// Routing never signs: a key-only wallet is enough to construct the clients.
const wallet = new ProtoWallet(PrivateKey.fromRandom()) as unknown as WalletInterface

afterEach(async () => {
resetToolboxConfig()
await AsyncStorage.clear()
})

describe('resolveMessageBoxUrl', () => {
it('gives each network its own server by default', () => {
expect(resolveMessageBoxUrl(null, 'main')).toBe('https://messagebox.bsvblockchain.tech')
expect(resolveMessageBoxUrl(null, 'test')).toBe('https://messagebox-testnet.bsvblockchain.tech')
expect(resolveMessageBoxUrl(null, 'teratest')).toBe('https://messagebox-ttn.bsvblockchain.tech')
expect(resolveMessageBoxUrl(null, 'scaletest')).toBe('https://messagebox-tstn.bsvblockchain.tech')
})

// Until each network had a server, every network used mainnet's: a test
// profile that saved it saved the default, not a choice of mainnet's box.
it("reads any network's default, or the retired one, as the default", () => {
expect(resolveMessageBoxUrl(DEFAULT_MESSAGE_BOX_URL, 'teratest')).toBe('https://messagebox-ttn.bsvblockchain.tech')
expect(resolveMessageBoxUrl('https://messagebox-testnet.bsvblockchain.tech', 'main')).toBe(DEFAULT_MESSAGE_BOX_URL)
expect(resolveMessageBoxUrl(LEGACY_MESSAGE_BOX_URL, 'test')).toBe('https://messagebox-testnet.bsvblockchain.tech')
})

it('keeps a server the user chose, and keeps MessageBox off when they turned it off', () => {
expect(resolveMessageBoxUrl('https://mb.example.com', 'teratest')).toBe('https://mb.example.com')
expect(resolveMessageBoxUrl(NO_MESSAGE_BOX, 'teratest')).toBeUndefined()
})

it("uses the host's configured default, and only a safe one", () => {
configureToolbox({ backupUrl: null, services: { teratest: { messageBoxUrl: 'https://mb.example.com/' } } })
expect(defaultMessageBoxUrlFor('teratest')).toBe('https://mb.example.com')
configureToolbox({ backupUrl: null, services: { teratest: { messageBoxUrl: 'http://mb.example.com' } } })
expect(defaultMessageBoxUrlFor('teratest')).toBe('https://messagebox-ttn.bsvblockchain.tech')
})

it('reads the named profile’s preference', async () => {
await AsyncStorage.setItem('message_box_url__p2', 'https://mb.example.com')
expect(await readMessageBoxUrl('test', 2)).toBe('https://mb.example.com')
expect(await readMessageBoxUrl('test')).toBe('https://messagebox-testnet.bsvblockchain.tech')
})
})

describe('bindMessageBoxNetwork', () => {
// Pins the two fields MessageBoxClient reads for routing. If an upgrade
// renames them this fails, instead of routing through mainnet's overlay again.
it('points a PeerPayClient at the network’s overlay — the constructor alone cannot', () => {
const client = new PeerPayClient({
messageBoxHost: 'https://messagebox-ttn.bsvblockchain.tech',
walletClient: wallet
})
expect((client as any).networkPreset).toBe('mainnet')
bindMessageBoxNetwork(client, 'teratest')
expect((client as any).networkPreset).toBe('teratestnet')
expect((client as any).lookupResolver).toBeInstanceOf(LookupResolver)
})

it('routes to the configured host when the network has no overlay', async () => {
const client = bindMessageBoxNetwork(
new MessageBoxClient({ host: 'https://messagebox-tstn.bsvblockchain.tech', walletClient: wallet }),
'scaletest'
)
expect((client as any).networkPreset).toBe('local')
const recipient = PrivateKey.fromRandom().toPublicKey().toString()
await expect(client.resolveHostForRecipient(recipient)).resolves.toBe('https://messagebox-tstn.bsvblockchain.tech')
})

it('makePeerPayClient binds the client it builds', () => {
const client = makePeerPayClient({
wallet,
messageBoxUrl: 'https://messagebox-testnet.bsvblockchain.tech',
network: 'test'
})
expect((client as any).networkPreset).toBe('testnet')
})
})
Loading
Loading