Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
60 changes: 60 additions & 0 deletions .env.devnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Environment for the /devnet deployment ("staging-devnet") and local devnet runs.
#
# Loaded by `npm run build:devnet`, which sources this file before running
# `next build` with BASE_PATH=/devnet. Next.js does not auto-load `.env.devnet`,
# so the values below only take effect through that script. The same file is read
# by scripts/sdk-env.mjs, so `NETWORK=devnet node scripts/<script>.mjs` picks up
# the same wiring without repeating it on the command line.
#
# This file is checked in on purpose: contract IDs, identity IDs and node
# addresses are public data. Never put a seed phrase, WIF, or any other private
# key material here — those live in `.env.local` (gitignored) or GitHub secrets.

NEXT_PUBLIC_NETWORK=devnet
NEXT_PUBLIC_DEVNET_NAME=moutai

# moutai has no masternode discovery, so the DAPI pool is listed explicitly.
# Five seed hostnames round-robin over three machines; only :1443 is open, and it
# serves a valid public TLS certificate.
NEXT_PUBLIC_DAPI_ADDRESSES=https://seed-1.moutai.networks.dash.org:1443,https://seed-2.moutai.networks.dash.org:1443,https://seed-3.moutai.networks.dash.org:1443,https://seed-4.moutai.networks.dash.org:1443,https://seed-5.moutai.networks.dash.org:1443

# Quorum service for the trusted context (BLS public keys used to verify read
# proofs). REQUIRED — without it every read fails: wasm-sdk 4.2.0-dev.2 panics on
# `proofs: false` ("queries without proofs are not supported yet") and refuses
# non-trusted proof verification ("Non-trusted mode is not supported in WASM").
# The SDK's default host, quorums.moutai.networks.dash.org, does not exist
# (NXDOMAIN as of 2026-08-27), so a service exposing /quorums, /previous and
# /masternodes has to be stood up and named here before /devnet can work.
# NEXT_PUBLIC_QUORUM_URL=

# Insight reports "network":"testnet" — devnets reuse testnet address and WIF
# prefixes, which is why key derivation stays on testnet (see keyNetwork()).
NEXT_PUBLIC_INSIGHT_API_URL=https://insight.moutai.networks.dash.org/insight-api

# Contracts on moutai. Published by:
# NETWORK=devnet node scripts/register-test-contracts.mjs \
# --source-network testnet \
# --from-social 9oDC6xdg8WRixTD2j3FCBq3vtsrf6bRGjXSJbhtFoma9 \
# --from-profile FZSnZdKsLAuWxE7iZJq12eEz6xfGTgKPxK7uZJapTQxe \
# --owner <devnetMakerId> --owner-index 9
# followed by scripts/set-yapp-price.mjs to set the YAPP direct-purchase price
# (1,000,000 credits per token, minimum 100 per purchase).
# Left blank until moutai can be read at all — the build falls back to the
# testnet contract IDs in lib/constants.ts, which do NOT exist on moutai.
# NEXT_PUBLIC_YAPPR_CONTRACT_ID=
# NEXT_PUBLIC_YAPPR_PROFILE_CONTRACT_ID=

# DPNS is a system contract and normally keeps the same id on every chain; set
# this only if the devnet registered it elsewhere.
# NEXT_PUBLIC_DPNS_CONTRACT_ID=

# Contract-maker identity: seed index 9, so it is recoverable from E2E_SEED_PHRASE
# alone (unlike testnet's maker, whose key lives in a ~/Downloads JSON).
# DEVNET_MAKER_IDENTITY_ID=

# Comma-separated identity IDs of the devnet bot pool, in derivation-index order
# (seed indices 0 and 1). Identity IDs come from the asset-lock outpoint, so a
# devnet bot has a different id than its testnet counterpart at the same index.
# E2E_IDENTITY_IDS=

NEXT_PUBLIC_LOG_LEVEL=debug
33 changes: 28 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ name: Deploy to GitHub Pages
# Unified deploy: the Pages artifact always contains the production site
# (built from master, served at the root / yap.pr), the testing site (built
# from the same master commit against the test contracts, served under
# /testing) and the staging site (built from the staging branch, served under
# /staging). A push to either branch rebuilds all three so no deploy can wipe
# another.
# /testing), the staging site (built from the staging branch, served under
# /staging) and the devnet site (the same staging commit built against
# .env.devnet, served under /devnet). A push to either branch rebuilds all four
# so no deploy can wipe another.

on:
# Master deploys are gated on CI: CI runs on every push to master, and this
Expand Down Expand Up @@ -87,9 +88,25 @@ jobs:
run: |
npm ci
npm run build
mv out out-staging
env:
BASE_PATH: /staging

# Same staging checkout and the same commit as the /staging build, rebuilt
# against .env.devnet (BASE_PATH=/devnet, so client-side storage is scoped
# to `devnet:` and stays isolated from / , /staging and /testing).
# continue-on-error because the devnet chain is disposable and resets
# without notice — a broken /devnet must never hold back /staging.
- name: Build devnet (staging)
id: devnet-build
if: steps.staging-build.outcome == 'success'
continue-on-error: true
working-directory: staging
run: |
rm -rf .next
npm run build:devnet
mv out out-devnet

# Same master checkout and the same commit as the production build, so no
# second `npm ci` — but the production output has to be moved aside first
# and .next dropped so the /testing build cannot inherit its artifacts.
Expand All @@ -107,12 +124,18 @@ jobs:
cp -R master/out-production/. site/
mkdir -p site/testing
cp -R master/out/. site/testing/
if [ "${{ steps.staging-build.outcome }}" = "success" ] && [ -d staging/out ]; then
if [ "${{ steps.staging-build.outcome }}" = "success" ] && [ -d staging/out-staging ]; then
mkdir -p site/staging
cp -R staging/out/. site/staging/
cp -R staging/out-staging/. site/staging/
else
echo "::warning::staging build failed — deploying without an updated /staging"
fi
if [ "${{ steps.devnet-build.outcome }}" = "success" ] && [ -d staging/out-devnet ]; then
mkdir -p site/devnet
cp -R staging/out-devnet/. site/devnet/
else
echo "::warning::devnet build failed or was skipped — deploying without an updated /devnet"
fi

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ tmp/
# Test identity files (contain private keys)
testing-identity-*.json

# One-shot asset-lock private keys recorded by scripts/provision-test-identity.mjs
.devnet-locks.local

# Rust build artifacts
target/
Cargo.lock
Expand Down
6 changes: 3 additions & 3 deletions components/auth/add-encryption-key-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
import { useAuth } from '@/contexts/auth-context'
import toast from 'react-hot-toast'
import { YAPPR_CONTRACT_ID } from '@/lib/constants'
import { YAPPR_CONTRACT_ID, keyNetwork } from '@/lib/constants'

type EncryptionKeyContext = 'private-feed' | 'store' | 'generic'

Expand Down Expand Up @@ -166,7 +166,7 @@ export function AddEncryptionKeyModal({
if (!isOpenRef.current) return

if (matches) {
const network = (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'
const network = keyNetwork()
const derivedKeyWif = privateKeyToWif(derivedKey, network, true)
const { storeEncryptionKey, storeEncryptionKeyType } = await import('@/lib/secure-storage')
storeEncryptionKey(user.identityId, derivedKeyWif)
Expand Down Expand Up @@ -248,7 +248,7 @@ export function AddEncryptionKeyModal({
// Derive encryption key using HKDF
const { deriveEncryptionKey } = await import('@/lib/crypto/key-derivation')
const encryptionKeyBytes = deriveEncryptionKey(authPrivateKey, user.identityId)
const network = (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'
const network = keyNetwork()
const encryptionKeyWif = privateKeyToWif(encryptionKeyBytes, network, true)

setPrivateKeyBytes(encryptionKeyBytes)
Expand Down
3 changes: 2 additions & 1 deletion components/auth/encryption-key-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AddEncryptionKeyModal } from './add-encryption-key-modal'
import { LostEncryptionKeyModal } from './lost-encryption-key-modal'
import { identityService } from '@/lib/services/identity-service'
import toast from 'react-hot-toast'
import { keyNetwork } from '@/lib/constants'

type AutoRecoveryStatus = 'idle' | 'checking' | 'found' | 'failed'

Expand Down Expand Up @@ -106,7 +107,7 @@ export function EncryptionKeyModal() {

// Convert to hex for storage
const { privateKeyToWif } = await import('@/lib/crypto/wif')
const network = (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'
const network = keyNetwork()
const derivedKeyWif = privateKeyToWif(derivedKey, network, true)

storeEncryptionKey(user.identityId, derivedKeyWif)
Expand Down
3 changes: 2 additions & 1 deletion components/dpns/registration-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CheckingStep } from './steps/checking-step'
import { ReviewStep } from './steps/review-step'
import { RegisteringStep } from './steps/registering-step'
import { CompleteStep } from './steps/complete-step'
import { keyNetwork } from '@/lib/constants'

interface DpnsRegistrationWizardProps {
onComplete?: () => void
Expand Down Expand Up @@ -144,7 +145,7 @@ export function DpnsRegistrationWizard({ onComplete, onSkip, hasExistingUsername
}

// Determine network from environment
const network = (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'
const network = keyNetwork()

// Convert identity public keys to the format expected by findMatchingKeyIndex
const keyInfos = convertToKeyInfo(identity.publicKeys)
Expand Down
13 changes: 9 additions & 4 deletions contexts/sdk-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { logger } from '@/lib/logger';
import React, { createContext, useContext, useEffect, useState } from 'react'
import { evoSdkService } from '@/lib/services/evo-sdk-service'
import { YAPPR_CONTRACT_ID } from '@/lib/constants'
import { YAPPR_CONTRACT_ID, getConfiguredNetwork } from '@/lib/constants'

interface SdkContextType {
isReady: boolean
Expand All @@ -19,11 +19,16 @@ export function SdkProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const initializeSdk = async () => {
try {
logger.info('SdkProvider: Starting EvoSDK initialization for testnet...')
// This provider is the app-wide SDK bootstrap and usually wins the race
// against the on-demand callers (DashPlatformClient, platform-auth), so
// it has to agree with them on the network. Hardcoding it would leave a
// /devnet build reading testnet through every `useSdk()` consumer until
// some later caller forced a reinit.
const network = getConfiguredNetwork()
logger.info(`SdkProvider: Starting EvoSDK initialization for ${network}...`)

// Initialize with testnet configuration
await evoSdkService.initialize({
network: 'testnet',
network,
contractId: YAPPR_CONTRACT_ID
})

Expand Down
11 changes: 4 additions & 7 deletions lib/auth/platform-auth-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
deriveYapprEncryptionKeyFromLogin,
} from 'platform-auth'
import { logger } from '@/lib/logger'
import { KEY_EXCHANGE_CONTRACT_ID, YAPPR_CONTRACT_ID } from '@/lib/constants'
import { KEY_EXCHANGE_CONTRACT_ID, YAPPR_CONTRACT_ID, getConfiguredNetwork, keyNetwork } from '@/lib/constants'
import { evoSdkService } from '@/lib/services/evo-sdk-service'
import {
clearAuthVaultDek,
Expand Down Expand Up @@ -81,10 +81,6 @@ type LegacyAuthVaultBundle = {
updatedAt: number
}

function getConfiguredNetwork(): 'testnet' | 'mainnet' {
return (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'
}

async function ensureSdk(): Promise<void> {
await evoSdkService.initialize({
network: getConfiguredNetwork(),
Expand Down Expand Up @@ -248,7 +244,8 @@ async function runLogoutCleanup(identityId: string): Promise<void> {

export function createYapprPlatformAuthDependencies(): PlatformAuthDependencies {
return {
network: getConfiguredNetwork(),
// platform-auth uses this only for address/WIF encoding, so devnet maps to testnet.
network: keyNetwork(),
sessionStore: {
getSession() {
if (typeof window === 'undefined') return null
Expand Down Expand Up @@ -366,7 +363,7 @@ export function createYapprPlatformAuthDependencies(): PlatformAuthDependencies
yapprKeyExchangeConfig: {
appContractId: YAPPR_CONTRACT_ID,
keyExchangeContractId: KEY_EXCHANGE_CONTRACT_ID,
network: getConfiguredNetwork(),
network: keyNetwork(),
label: 'Login to Yappr',
},
yapprKeyExchange: {
Expand Down
46 changes: 43 additions & 3 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export const YAPP_TOKEN_COSTS = {
export const YAPPR_PROFILE_CONTRACT_ID = process.env.NEXT_PUBLIC_YAPPR_PROFILE_CONTRACT_ID || 'FZSnZdKsLAuWxE7iZJq12eEz6xfGTgKPxK7uZJapTQxe' // Unified profile contract
export const YAPPR_DM_CONTRACT_ID = process.env.NEXT_PUBLIC_YAPPR_DM_CONTRACT_ID || 'J7MP9YU1aEGNAe7bjB45XdrjDLBsevFLPK1t1YwFS4ck' // Testnet - DM contract v3 (simplified readReceipt)
// YAPPR_BLOCK_CONTRACT_ID removed - block, blockFilter, blockFollow document types now in YAPPR_CONTRACT_ID
export const DPNS_CONTRACT_ID = 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec' // Testnet
// DPNS is a system contract, so its id is normally identical on every chain.
// Overridable all the same: a freshly genesised devnet can be brought up with a
// different DPNS registration, and `/devnet` must not preload a missing id.
export const DPNS_CONTRACT_ID = process.env.NEXT_PUBLIC_DPNS_CONTRACT_ID || 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec'
export const YAPPR_STOREFRONT_CONTRACT_ID = process.env.NEXT_PUBLIC_YAPPR_STOREFRONT_CONTRACT_ID || '2AUBj86MGTsXP7A3ekD62YoTeDwtJe5b9MxwkWwdg6Ba' // Testnet - Storefront contract v2 (with savedAddress)
export const ENCRYPTED_KEY_BACKUP_CONTRACT_ID = process.env.NEXT_PUBLIC_ENCRYPTED_KEY_BACKUP_CONTRACT_ID || '8fmYhuM2ypyQ9GGt4KpxMc9qe5mLf55i8K3SZbHvS9Ts' // Testnet - Encrypted key backup contract (1B max iterations)
// HASHTAG_CONTRACT_ID and MENTION_CONTRACT_ID removed - these document types are now in YAPPR_CONTRACT_ID
Expand Down Expand Up @@ -81,12 +84,49 @@ export const APP_URL = 'https://yap.pr'
export const LEGACY_APP_URL = 'https://yappr-v2.thepasta.org'

// Network configuration
export const DEFAULT_NETWORK = 'testnet'
//
// `AppNetwork` is what the SDK connects to; `KeyNetwork` is what address and WIF
// encoding follow. They differ on devnet: Dash devnets reuse the testnet address
// and WIF version bytes (moutai's Insight even reports `"network":"testnet"`), so
// every key-derivation and secure-storage call site must stay on 'testnet' there.
// Use `getConfiguredNetwork()` for connections and `keyNetwork()` for key material.
export type AppNetwork = 'testnet' | 'mainnet' | 'devnet'
export type KeyNetwork = 'testnet' | 'mainnet'

export const DEFAULT_NETWORK: AppNetwork = 'testnet'

/** The network the SDK talks to, from `NEXT_PUBLIC_NETWORK`. */
export function getConfiguredNetwork(): AppNetwork {
const configured = process.env.NEXT_PUBLIC_NETWORK
if (configured === 'mainnet' || configured === 'devnet' || configured === 'testnet') {
return configured
}
return DEFAULT_NETWORK
}

/** The network whose address/WIF prefixes apply. Devnets use testnet's. */
export function keyNetwork(): KeyNetwork {
return getConfiguredNetwork() === 'mainnet' ? 'mainnet' : 'testnet'
}

// Devnet wiring. A devnet has no public masternode discovery, so the DAPI
// addresses are supplied explicitly and the trusted context (quorum public keys)
// is prefetched from a quorum service. `EvoSDK.devnetTrusted` defaults that to
// `https://quorums.<devnetName>.networks.dash.org`, which does not exist for
// moutai — point NEXT_PUBLIC_QUORUM_URL at a service exposing /quorums,
// /previous and /masternodes instead.
export const DEVNET_NAME = process.env.NEXT_PUBLIC_DEVNET_NAME || 'moutai'
export const DEVNET_QUORUM_URL = process.env.NEXT_PUBLIC_QUORUM_URL || ''
export const DAPI_ADDRESSES: readonly string[] = (process.env.NEXT_PUBLIC_DAPI_ADDRESSES || '')
.split(',')
.map((address) => address.trim())
.filter(Boolean)

// Insight API configuration for transaction detection
export const INSIGHT_API_URLS = {
testnet: 'https://insight.testnet.networks.dash.org/insight-api',
mainnet: 'https://insight.dash.org/insight-api'
mainnet: 'https://insight.dash.org/insight-api',
devnet: process.env.NEXT_PUBLIC_INSIGHT_API_URL || 'https://insight.moutai.networks.dash.org/insight-api',
} as const

export const INSIGHT_API_CONFIG = {
Expand Down
4 changes: 2 additions & 2 deletions lib/dash-platform-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { logger } from '@/lib/logger';
// Import the centralized SDK service
import { evoSdkService } from './services/evo-sdk-service'
import { YAPPR_CONTRACT_ID } from './constants'
import { YAPPR_CONTRACT_ID, getConfiguredNetwork } from './constants'
import { SESSION_STORAGE_KEY } from './storage-scope'
import { documentToPlainObject } from './services/sdk-helpers'

Expand Down Expand Up @@ -36,7 +36,7 @@ export class DashPlatformClient {

try {
// Use the centralized WASM service
const network = (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet'
const network = getConfiguredNetwork()
const contractId = YAPPR_CONTRACT_ID

logger.info('DashPlatformClient: Initializing via WasmSdkService for network:', network)
Expand Down
11 changes: 3 additions & 8 deletions lib/secure-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@ import {
} from 'platform-auth'
import { isLikelyWif, parsePrivateKey, privateKeyToWif } from '@/lib/crypto/wif'
import { scopedKey } from '@/lib/storage-scope'

const getConfiguredNetwork = (): 'testnet' | 'mainnet' => {
if (process?.env?.NEXT_PUBLIC_NETWORK) {
return process.env.NEXT_PUBLIC_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'
}
return 'testnet'
}
import { keyNetwork } from '@/lib/constants'

const browserSecretStore = createBrowserSecretStore({
prefix: scopedKey('yappr_secure_'),
network: getConfiguredNetwork(),
// Stored secrets are WIF-encoded, so this follows the key network: devnet reuses testnet's prefixes.
network: keyNetwork(),
crypto: {
parsePrivateKey,
privateKeyToWif,
Expand Down
4 changes: 2 additions & 2 deletions lib/services/dpns-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { logger } from '@/lib/logger';
import { getEvoSdk } from './evo-sdk-service';
import { SecurityLevel, KeyPurpose, signerService } from './signer-service';
import { DPNS_CONTRACT_ID, DPNS_DOCUMENT_TYPE } from '../constants';
import { DPNS_CONTRACT_ID, DPNS_DOCUMENT_TYPE, keyNetwork } from '../constants';
import { documentToPlainObject, identifierToBase58 } from './sdk-helpers';
import { findMatchingKeyIndex, getSecurityLevelName, type IdentityPublicKeyInfo } from '@/lib/crypto/keys';
import type { UsernameCheckResult, UsernameRegistrationResult } from '../types';
Expand Down Expand Up @@ -402,7 +402,7 @@ class DpnsService {
wasmPublicKeys: WasmIdentityPublicKey[],
requiredSecurityLevel: number = SecurityLevel.CRITICAL
): WasmIdentityPublicKey | null {
const network = (process.env.NEXT_PUBLIC_NETWORK as 'testnet' | 'mainnet') || 'testnet';
const network = keyNetwork();

// Filter out disabled keys before processing
const activeWasmKeys = wasmPublicKeys.filter(k => !k.disabledAt);
Expand Down
Loading