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 deployments/core/cli-deposit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { fileURLToPath } from 'node:url'
import { afterEach, beforeEach, expect, test, vi } from 'vitest'

import { loadEarnProfileBundle } from './bundle.js'

const { depositEarnProfile, verifyEarnProfile } = vi.hoisted(() => ({
depositEarnProfile: vi.fn(),
verifyEarnProfile: vi.fn(),
}))
vi.mock('./deposit.js', async (actual) => ({
...(await actual<typeof import('./deposit.js')>()),
depositEarnProfile,
}))
vi.mock('./deploy.js', async (actual) => ({
...(await actual<typeof import('./deploy.js')>()),
verifyEarnProfile,
}))

const { main } = await import('./cli.js')

const profilePath = fileURLToPath(
new URL('../../profiles/veda-moderato/profile.json', import.meta.url),
)
const deposit = [
'deposit',
profilePath,
'--assets',
'1',
'--rpc-url',
'http://127.0.0.1:1',
'--json',
]
const output = { log: vi.fn(), warn: vi.fn() }

beforeEach(() => {
vi.stubEnv('DEPLOYER_PRIVATE_KEY', '')
depositEarnProfile.mockReset().mockResolvedValue({ state: 'preview' })
verifyEarnProfile.mockReset()
})
afterEach(() => vi.unstubAllEnvs())

test('broadcast refuses to fund a record that fails verification', async () => {
verifyEarnProfile.mockRejectedValue(
new Error('Runtime bytecode hash changed for addresses.earnVault'),
)
await expect(main([...deposit, '--broadcast'], output, '/')).rejects.toThrow(
'Runtime bytecode hash changed',
)
expect(verifyEarnProfile).toHaveBeenCalledOnce()
expect(depositEarnProfile).not.toHaveBeenCalled()
})

test('broadcast funds only the verified record', async () => {
const verified = { addresses: {} }
verifyEarnProfile.mockResolvedValue({ record: verified })
await main([...deposit, '--broadcast', '--skip-zone'], output, '/')
expect(verifyEarnProfile).toHaveBeenCalledWith(
expect.objectContaining({ profilePath, rpcUrl: 'http://127.0.0.1:1', skipLive: ['zone'] }),
)
expect(depositEarnProfile).toHaveBeenCalledWith(
expect.objectContaining({ broadcast: true, record: verified }),
)
})

test('a preview rejects live-check skips it would ignore', async () => {
await expect(main([...deposit, '--skip-zone'], output, '/')).rejects.toThrow(
'--skip-zone is supported only by verify or deposit --broadcast',
)
expect(depositEarnProfile).not.toHaveBeenCalled()
})

test('a preview quotes the recorded stack without verification', async () => {
await main(deposit, output, '/')
expect(verifyEarnProfile).not.toHaveBeenCalled()
expect(depositEarnProfile).toHaveBeenCalledWith(
expect.objectContaining({
broadcast: false,
record: loadEarnProfileBundle(profilePath).record,
}),
)
})
2 changes: 1 addition & 1 deletion deployments/core/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('earn CLI', () => {

test('accepts --skip-zone only on verify', async () => {
await expect(main(['deploy', 'profile.json', '--skip-zone'], console, '/')).rejects.toThrow(
'--skip-zone is supported only by verify',
'--skip-zone is supported only by verify or deposit --broadcast',
)
})
})
Expand Down
23 changes: 19 additions & 4 deletions deployments/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ const usage =
'Usage: pnpm earn <plan|deploy|status|verify|deposit> <profile.json> [--trusted-identity <path>] [--rpc-url <url>] [--watch] [--json]\n' +
' pnpm earn deploy <profile.json> [--resubmit-verification] [--rpc-url <url>] [--json]\n' +
` pnpm earn verify <profile.json> ${skipLiveFlags.map((flag) => `[--skip-${flag}]`).join(' ')} [--resubmit-verification] [--rpc-url <url>] [--json]\n` +
' pnpm earn deposit <profile.json> --assets <amount> [--account <address>] [--receiver <address>] [--slippage-bps <bps>] [--broadcast] [--rpc-url <url>] [--json]\n' +
` pnpm earn deposit <profile.json> --assets <amount> ${skipLiveFlags.map((flag) => `[--skip-${flag}]`).join(' ')}` +
' [--account <address>] [--receiver <address>] [--slippage-bps <bps>] [--broadcast] [--rpc-url <url>] [--json]\n' +
capabilityCommands.map((command) => ` ${command.usage.replace('Usage: ', '')}`).join('\n')

type Command = 'deploy' | 'deposit' | 'plan' | 'status' | 'verify'
Expand Down Expand Up @@ -191,7 +192,21 @@ export async function main(
}

if (command === 'deposit') {
const record = loadProfileDeploymentRecord(profilePath, profile)
// Funding signs against the recorded stack, so authenticate it as verify does first: live
// provenance, source verification, and the stack the reviewed factories predict. A preview
// signs nothing and may quote a record whose source verification is still pending.
const record = options.broadcast
? (
await verifyEarnProfile({
environment,
profilePath,
root: repositoryRoot,
rpcUrl,
trustedIdentity,
skipLive: options.skipLive,
})
).record
: loadProfileDeploymentRecord(profilePath, profile)
const result = await depositEarnProfile({
account: options.account,
assets: options.assets!,
Expand Down Expand Up @@ -302,8 +317,8 @@ function parseArguments(command: Command, args: string[]) {
const skipLive = skipLiveFlags.filter(
(flag) => (values as Record<string, unknown>)[`skip-${flag}`],
)
if (command !== 'verify' && skipLive.length > 0)
throw new Error(`--skip-${skipLive[0]} is supported only by verify`)
if (skipLive.length > 0 && command !== 'verify' && !(command === 'deposit' && broadcast))
throw new Error(`--skip-${skipLive[0]} is supported only by verify or deposit --broadcast`)
return {
account,
assets,
Expand Down
5 changes: 4 additions & 1 deletion deployments/modules/earn/zone-router/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export async function discoverZoneOutputs(options: {
? zoneRpcConfiguration(zone.chainId, options.environment)
: undefined
if (requireLiveZone && !zoneRpc?.url) {
throw missingZoneRpcError(zone, 'pnpm earn verify --skip-zone checks everything else.')
throw missingZoneRpcError(
zone,
'Pass --skip-zone to verify or deposit --broadcast to check everything else.',
)
}
const identity = await inspectZone({
id: zone.id,
Expand Down
7 changes: 5 additions & 2 deletions docs/deployment/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ neither supplies a value it needs:
`plan` and `status` use the identity in `deployment.json` or `deployment.pending.json` without a key;
- `TEMPO_RPC_URL` (or `--rpc-url`), for `deploy`, `verify`, and `deposit`.

`verify` of a Zone profile also needs `TEMPO_API_KEY` or `ZONE_RPC_URL` unless `--skip-zone` is
`verify` and `deposit --broadcast` of a Zone profile also need `TEMPO_API_KEY` or `ZONE_RPC_URL` unless `--skip-zone` is
passed.

`ZONE_ADMIN_PRIVATE_KEY` is optional: when set, `deploy` performs the Zone administrator's steps
Expand All @@ -65,7 +65,10 @@ pnpm earn deposit profiles/e2e-moderato/profile.json --assets 1 --broadcast
`--account <address>` quotes for another account without that key, and `--receiver <address>`
mints elsewhere. The minimum output defaults to 50 bps below the quote (`--slippage-bps`, 0 to
9999). The quote uses one block snapshot and includes pending fee dilution; a zero-share minimum
is refused. It refuses an RPC whose chain or genesis differs from the record. Nothing is sent
is refused. With `--broadcast`, it first runs the same checks as `verify` (record provenance,
source verification, and the stack the reviewed factories predict) and refuses to sign for a
record that fails them; `--skip-zone` applies as it does for `verify`. It refuses an RPC whose
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
chain or genesis differs from the record. Nothing is sent
without `--broadcast`, and approval and deposit are one Tempo transaction. A hosted record with
pending source verification may be quoted but cannot be funded until verification completes; local
records whose verification is not applicable remain usable. The creation engine must match the
Expand Down
Loading