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
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/dist/**
**/node_modules/**
lib/**
packages/mppx-stake/src/abi/MPPEscrow.ts
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"trailingComma": "all",
"arrowParens": "avoid",
"tabWidth": 2,
"printWidth": 80
"printWidth": 80,
"endOfLine": "auto"
}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This file gives agent operators quick routing for work in this repository.
- `apps/`
- `apps/demo/`: end-user UI demo
- `apps/cli/`: command-line tooling
- `apps/mpp-server/`: standalone Express demo server that gates a fake document behind a `tempo/stake` paywall
- `apps/mpp-server/`: standalone Express demo server that gates a fake document behind an `evm/stake` paywall
- `packages/`
- `packages/mppx-stake/`: MPP stake TypeScript SDK + ABI/type exports
- `packages/ui/`: shared UI components
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ import { serverStake } from '@gitbondhq/mppx-stake/server'
import { Mppx } from 'mppx/server'

const mppx = Mppx.create({
methods: [serverStake({ contract, counterparty, token, name, chainId })],
methods: [serverStake({ contract, counterparty, token, chainId })],
secretKey: process.env.MPP_SECRET_KEY!,
})
```
Expand All @@ -110,7 +110,6 @@ Edit `config.json` at the repo root:

```json
{
"methodName": "tempo",
"chainId": 42431,
"escrow": {
"contract": "0x3E7f...",
Expand Down Expand Up @@ -141,7 +140,7 @@ import { Mppx } from 'mppx/client'
import { clientStake } from '@gitbondhq/mppx-stake/client'

const mppx = Mppx.create({
methods: [clientStake({ name, beneficiaryAccount })],
methods: [clientStake({ beneficiaryAccount })],
})
```

Expand All @@ -152,7 +151,7 @@ import { Mppx } from 'mppx/server'
import { serverStake } from '@gitbondhq/mppx-stake/server'

const mppx = Mppx.create({
methods: [serverStake({ contract, counterparty, token, name, chainId })],
methods: [serverStake({ contract, counterparty, token, chainId })],
secretKey: process.env.MPP_SECRET_KEY!,
})
```
Expand Down
24 changes: 10 additions & 14 deletions apps/cli/src/cli/challenge.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { access, mkdir, readFile, readdir } from 'node:fs/promises'
import { access, mkdir, readdir, readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'

import { parseStakeChallenge, type StakeChallenge } from '@gitbondhq/mppx-stake'
import { Credential } from 'mppx'

import { repoConfig, resolveProtectedResourceUrl } from './context.js'
import { resolveProtectedResourceUrl } from './context.js'
import { fetchWithOptions } from './http.js'
import { requiredString } from './parsing.js'

export const defaultChallengesDirectory = 'challenges'
export const defaultCredentialFilePath = 'credential.txt'
Expand Down Expand Up @@ -48,9 +47,7 @@ export async function loadStakeChallengeFromFile(
? parsed.challenge
: parsed

return parseStakeChallenge(challenge, {
methodName: repoConfig.methodName,
})
return parseStakeChallenge(challenge)
}

export async function resolveSerializedCredential(options: {
Expand Down Expand Up @@ -96,20 +93,18 @@ async function fetchStakeChallenge(url: string): Promise<StakeChallenge> {
)
}

return parseStakeChallenge(response, {
methodName: repoConfig.methodName,
})
return parseStakeChallenge(response)
}

export async function resolveChallengeOutputPath(path?: string): Promise<string> {
export async function resolveChallengeOutputPath(
path?: string,
): Promise<string> {
const resolvedPath = path?.trim() || createTimestampedChallengeFilePath()
await mkdir(dirname(resolvedPath), { recursive: true })
return resolvedPath
}

export async function resolveChallengeFilePath(
path?: string,
): Promise<string> {
export async function resolveChallengeFilePath(path?: string): Promise<string> {
if (path?.trim()) {
return path.trim()
}
Expand Down Expand Up @@ -143,7 +138,8 @@ async function resolveLatestChallengeFilePath(): Promise<string | undefined> {
})
const latest = entries
.filter(
entry => entry.isFile() && entry.name.toLowerCase().endsWith('-challenge.json'),
entry =>
entry.isFile() && entry.name.toLowerCase().endsWith('-challenge.json'),
)
.map(entry => entry.name)
.sort()
Expand Down
7 changes: 1 addition & 6 deletions apps/cli/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type RepoConfig = {
token: Address
tokenWhitelist: Address[]
}
methodName: string
}

export function loadRepoConfig(repoConfigPath: URL): RepoConfig {
Expand All @@ -28,7 +27,6 @@ const parseRepoConfig = (value: unknown): RepoConfig => {
const raw = value as {
chainId?: unknown
escrow?: unknown
methodName?: unknown
}

if (
Expand All @@ -42,7 +40,6 @@ const parseRepoConfig = (value: unknown): RepoConfig => {
return {
chainId: raw.chainId,
escrow: parseEscrow(raw.escrow),
methodName: requiredString(raw.methodName, 'methodName'),
}
}

Expand Down Expand Up @@ -98,9 +95,7 @@ const requiredAddressArray = (value: unknown, label: string): Address[] => {
if (!Array.isArray(value) || value.length === 0) {
throw new Error(`config.json ${label} must be a non-empty address array.`)
}
return value.map((item, index) =>
requiredAddress(item, `${label}[${index}]`),
)
return value.map((item, index) => requiredAddress(item, `${label}[${index}]`))
}

const requiredBaseUnitAmount = (value: unknown, label: string): string => {
Expand Down
5 changes: 4 additions & 1 deletion apps/cli/src/cli/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@ export function resolveProtectedResourceUrl(url?: string): string {
const host = process.env.HOST?.trim() || defaultDemoHost
const port = process.env.PORT?.trim() || defaultDemoPort

return new URL(defaultProtectedResourcePath, `http://${host}:${port}`).toString()
return new URL(
defaultProtectedResourcePath,
`http://${host}:${port}`,
).toString()
}
57 changes: 25 additions & 32 deletions apps/cli/src/commands/challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
resolveSerializedCredential,
resolveStakeChallengeForRespond,
} from '../cli/challenge.js'
import { repoConfig, resolveProtectedResourceUrl } from '../cli/context.js'
import { resolveProtectedResourceUrl } from '../cli/context.js'
import { printJson, writeJsonFile } from '../cli/format.js'
import { fetchWithOptions, serializeHttpResponse } from '../cli/http.js'
import { resolveAccount, withSigningOptions } from '../cli/runtime.js'
Expand Down Expand Up @@ -44,11 +44,7 @@ export function registerChallengeCommands(program: Command): void {
})

const challengeValue =
response.status === 402
? parseStakeChallenge(response, {
methodName: repoConfig.methodName,
})
: null
response.status === 402 ? parseStakeChallenge(response) : null

const outputPath = challengeValue
? await resolveChallengeOutputPath(options.out)
Expand Down Expand Up @@ -119,10 +115,16 @@ export function registerChallengeCommands(program: Command): void {
const beneficiaryAccount = await resolveAccount(options)
const method = clientStake({
beneficiaryAccount,
name: repoConfig.methodName,
})
// `Challenge.fromMethod` widens `intent`/`method` to `string` in its
// return type, so the literal-typed `method.createCredential` parameter
// needs a cast here. Safe: `resolveStakeChallengeForRespond` always
// returns a stake challenge or throws.
type CredentialChallenge = Parameters<
typeof method.createCredential
>[0]['challenge']
const serializedCredential = await method.createCredential({
challenge: challengeValue,
challenge: challengeValue as CredentialChallenge,
})
const parsedCredential = Credential.deserialize<{
signature: string
Expand Down Expand Up @@ -158,28 +160,19 @@ export function registerChallengeCommands(program: Command): void {
'--url <url>',
'Protected resource URL. Defaults to MPP_RESOURCE_URL or the local demo server route.',
)
.action(
async (options: {
credentialFile?: string
url?: string
}) => {
const credential = await resolveSerializedCredential(options)
const response = await fetchWithOptions({
authorization: credential,
url: resolveProtectedResourceUrl(options.url),
})

const challengeValue =
response.status === 402
? parseStakeChallenge(response, {
methodName: repoConfig.methodName,
})
: null

printJson({
...(challengeValue ? { challenge: challengeValue } : {}),
...(await serializeHttpResponse(response)),
})
},
)
.action(async (options: { credentialFile?: string; url?: string }) => {
const credential = await resolveSerializedCredential(options)
const response = await fetchWithOptions({
authorization: credential,
url: resolveProtectedResourceUrl(options.url),
})

const challengeValue =
response.status === 402 ? parseStakeChallenge(response) : null

printJson({
...(challengeValue ? { challenge: challengeValue } : {}),
...(await serializeHttpResponse(response)),
})
})
}
15 changes: 12 additions & 3 deletions apps/cli/src/commands/escrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function registerEscrowCommands(program: Command): void {
escrow
.command('create-escrow')
.description('Call MPPEscrow.createEscrow')
.requiredOption('--scope <bytes32>', 'Escrow scope as a 32-byte hex value')
.requiredOption(
'--scope <bytes32>',
'Escrow scope as a 32-byte hex value',
)
.requiredOption('--counterparty <address>', 'Counterparty address')
.option(
'--beneficiary <address>',
Expand Down Expand Up @@ -133,7 +136,10 @@ export function registerEscrowCommands(program: Command): void {
escrow
.command('get-active-escrow-id')
.description('Call MPPEscrow.getActiveEscrowId')
.requiredOption('--scope <bytes32>', 'Escrow scope as a 32-byte hex value')
.requiredOption(
'--scope <bytes32>',
'Escrow scope as a 32-byte hex value',
)
.requiredOption('--beneficiary <address>', 'Beneficiary address'),
).action(
async (
Expand Down Expand Up @@ -163,7 +169,10 @@ export function registerEscrowCommands(program: Command): void {
escrow
.command('get-active-escrow')
.description('Call MPPEscrow.getActiveEscrow')
.requiredOption('--scope <bytes32>', 'Escrow scope as a 32-byte hex value')
.requiredOption(
'--scope <bytes32>',
'Escrow scope as a 32-byte hex value',
)
.requiredOption('--beneficiary <address>', 'Beneficiary address'),
).action(
async (
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Environment variables:
${RESOURCE_URL_ENV} Default protected demo resource URL for challenge commands

Repo config:
config.json Shared methodName, chainId, optional rpcUrl, and escrow defaults at the repo root
config.json Shared chainId, optional rpcUrl, and escrow defaults at the repo root
`,
)

Expand Down
26 changes: 13 additions & 13 deletions apps/mpp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ npx mppx http://127.0.0.1:4020/documents/document

## Routes

| Route | Auth | Description |
|-------|------|-------------|
| `GET /healthz` | Public | Health check |
| `GET /` | Public | Server metadata + example commands |
| `GET /documents/document/preview` | Public | Document teaser |
| `GET /documents/document` | Stake | Full document (402 challenge if no credential) |
| Route | Auth | Description |
| --------------------------------- | ------ | ---------------------------------------------- |
| `GET /healthz` | Public | Health check |
| `GET /` | Public | Server metadata + example commands |
| `GET /documents/document/preview` | Public | Document teaser |
| `GET /documents/document` | Stake | Full document (402 challenge if no credential) |

## Flow

Expand Down Expand Up @@ -62,15 +62,15 @@ Examples:

**Required:**

| Variable | Description |
|----------|-------------|
| Variable | Description |
| ---------------- | ------------------------------------------- |
| `MPP_SECRET_KEY` | Secret for signing/verifying MPP challenges |

**Optional:**

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `4020` | Server port |
| `HOST` | `127.0.0.1` | Bind address |
| Variable | Default | Description |
| -------- | ----------- | ------------ |
| `PORT` | `4020` | Server port |
| `HOST` | `127.0.0.1` | Bind address |

Stake defaults are loaded directly from the repo-level `config.json` (`methodName`, `chainId`, and the `escrow` block) and parsed locally in `src/config.ts`. RPC endpoints fall back to viem's built-in default for the chain — set one explicitly in `serverStake({ rpcUrl })` if you need to point at a private node. The paywalled content lives in `apps/mpp-server/content/document.md`; the title comes from its H1 and the route slug comes from the filename.
Stake defaults are loaded directly from the repo-level `config.json` (`chainId` and the `escrow` block) and parsed locally in `src/config.ts`. RPC endpoints fall back to viem's built-in default for the chain — set one explicitly in `serverStake({ rpcUrl })` if you need to point at a private node. The paywalled content lives in `apps/mpp-server/content/document.md`; the title comes from its H1 and the route slug comes from the filename.
19 changes: 10 additions & 9 deletions apps/mpp-server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export type RepoConfig = {
token: Address
tokenWhitelist: Address[]
}
methodName: string
}

type ServerRepoConfig = RepoConfig & {
Expand Down Expand Up @@ -61,12 +60,18 @@ const getRequiredString = (name: string): string => {
}

const loadRepoConfig = (): ServerRepoConfig => {
const repoConfig = parseRepoConfig(JSON.parse(readFileSync(repoConfigPath, 'utf8')))
const repoConfig = parseRepoConfig(
JSON.parse(readFileSync(repoConfigPath, 'utf8')),
)
if (!repoConfig.escrow.contract) {
throw new Error('config.json escrow.contract is required for apps/mpp-server.')
throw new Error(
'config.json escrow.contract is required for apps/mpp-server.',
)
}
if (!repoConfig.escrow.counterparty) {
throw new Error('config.json escrow.counterparty is required for apps/mpp-server.')
throw new Error(
'config.json escrow.counterparty is required for apps/mpp-server.',
)
}
return repoConfig as ServerRepoConfig
}
Expand All @@ -79,7 +84,6 @@ const parseRepoConfig = (value: unknown): RepoConfig => {
const raw = value as {
chainId?: unknown
escrow?: unknown
methodName?: unknown
}

if (
Expand All @@ -93,7 +97,6 @@ const parseRepoConfig = (value: unknown): RepoConfig => {
return {
chainId: raw.chainId,
escrow: parseEscrow(raw.escrow),
methodName: requiredString(raw.methodName, 'methodName'),
}
}

Expand Down Expand Up @@ -149,9 +152,7 @@ const requiredAddressArray = (value: unknown, label: string): Address[] => {
if (!Array.isArray(value) || value.length === 0) {
throw new Error(`config.json ${label} must be a non-empty address array.`)
}
return value.map((item, index) =>
requiredAddress(item, `${label}[${index}]`),
)
return value.map((item, index) => requiredAddress(item, `${label}[${index}]`))
}

const requiredBaseUnitAmount = (value: unknown, label: string): string => {
Expand Down
Loading
Loading