Skip to content
Merged
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
81 changes: 81 additions & 0 deletions .env.devnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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, is live (stood up
# 2026-08-27, ELB), so the name-derived default works and this override is only
# needed if the service moves. First hits after idle can take ~10s (cold start);
# scripts warm the endpoints with a curl before connecting.
# 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).
# The social contract is the v3-DRAFT (v2 schemas + refersTo on
# follow.followingId and postMention.mentionedUserId; YAPP token block intact,
# direct-purchase price set to 1,000,000 credits/token). Registered 2026-08-27
# via scripts/register-social-v3-draft.mjs; profile cloned from testnet.
NEXT_PUBLIC_YAPPR_CONTRACT_ID=3414JJ3xGXK3Cgpy7NAdDXwAjvyoeD4bcBTX6nSh7ysg
NEXT_PUBLIC_YAPPR_PROFILE_CONTRACT_ID=DR5sJcSMj3jK9z32B7jRR62xmiHj7MdWym65NF4MoJp8

# Contracts NOT yet provisioned on moutai are explicitly blanked so their
# features fail closed (services report unconfigured / preload skips them)
# instead of querying testnet ids that do not exist on this chain. Provision a
# devnet copy and fill the id in to light the feature up.
NEXT_PUBLIC_YAPPR_DM_CONTRACT_ID=
NEXT_PUBLIC_YAPPR_STOREFRONT_CONTRACT_ID=
NEXT_PUBLIC_ENCRYPTED_KEY_BACKUP_CONTRACT_ID=
NEXT_PUBLIC_KEY_EXCHANGE_CONTRACT_ID=
NEXT_PUBLIC_YAPPR_VAULT_CONTRACT_ID=
NEXT_PUBLIC_YAPPR_AUTH_VAULT_CONTRACT_ID=
NEXT_PUBLIC_YAPPR_BLOG_CONTRACT_ID=
NEXT_PUBLIC_POLLR_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=DuqE3zgXprS5zU51YaB4GuGxTRzzukW59XAYKeM6gKGA

# The devnet social contract is owned by the devnet maker, so token moderation
# (freeze/unfreeze/destroy, price changes) gates on it instead of the testnet
# authority baked into lib/constants.ts.
NEXT_PUBLIC_YAPP_TOKEN_AUTHORITY_ID=DuqE3zgXprS5zU51YaB4GuGxTRzzukW59XAYKeM6gKGA

# 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=EEKarQ4hK3eiBGWNp1j11L7DMfdyFD6zfqaUktj81ikD,3NdmhtrNQA83jfUMTotE2pZyxSoYd1JxYqabNFdNRwGu

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
Loading