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
54 changes: 54 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,57 @@ passing
- `kyc`는 repo root에서 실행해야 한다(상대 fs_permissions + 스크립트 경로). CLI가
forge cwd를 repo root로 설정하고 artifact를 root-상대 경로로 전달한다.
- Non-goals: 프로덕션 key 관리, out/ ABI 커플링, 두 번째 web3 라이브러리 도입.

## CLI-002 — corner-store CLI v2 (preflight · trade surface · observability)

### Behavior

- 기존 `services/cli`에 명령 7개를 더한다(제품 코드/스크립트 변경 없음, `services/cli/**`
+ 문서만):
- `check <buyer> [--venue amm|rfq] [--amount n] [--json]` — 거래 없이 per-element
preflight. active manifest의 recipe id → `requiredElements()` →
`ElementRegistry.elementOf` → 각 element `check(buyer, seller, rwa, amount, "")`를
eth_call로 실행하고, `engine.evaluate(ctx)`(view)로 전체 verdict를 낸다. 표(id/name/
PASS·FAIL)+verdict 라인, FAIL 행은 디코딩된 reason, verdict가 rejected면 exit 1.
엔진은 `ctx.buyer`만 스크리닝(비-direction-aware)하므로 subject를 무시하는 asset-side
원소(B-01/B-02/E-01)는 표에 `[asset-side]`로 표기해 per-buyer FAIL 오독을 막는다.
- `sell <amountIn> [--min]` — AMM 매도(tokenIn=RWA, tokenOut=QUOTE). `buy`를 미러링하되
`test/integration/SwapFlow.t.sol::test_sell_shaped_success`의 컨텍스트(ctx.buyer=매도자,
venueData=zeroForOne=false)를 그대로 따른다.
- `balances [addr...] [--json]` — RWA/QUOTE 잔고 + amm/rfq adapter allowance(기본: 5개
well-known 역할).
- `watch [--from block]` — `eth_getLogs` 폴링(~2s) 이벤트 tail: Executed, RFQFilled,
RFQQuoteCancelled, MakerApprovalSet, ManifestRegistered, ManifestStatusChanged,
SurveillanceFlag. reason/label·status·elementId 디코딩, `--from`은 히스토리 재생.
- `faucet <addr> <amount>` — QUOTE 민팅(MockERC20.mint permissionless, demo 전용).
- `snapshot` / `restore <id>` — anvil `evm_snapshot`/`evm_revert`(RPC 미지원 시 명확한 에러).
- `quote-inspect <file> [--json]` — 서명 quote 디코딩: services/rfq 타입드데이터로 서명자
복구(==maker PASS/FAIL), 만료 카운트다운, on-chain `usedQuoteNonce`/`approvedMaker`.
실패 검사가 하나라도 있으면 exit 1.
- 재사용 원칙 준수: reason 디코딩은 기존 `reason.ts`, EIP-712 복구는 services/rfq lib의
domain+types로 `ethers.verifyTypedData`(타입 문자열 재선언 없음). ABI fragment는 계속
hand-written(`out/` 비의존)이며 실제 컨트랙트 소스와 대조해 추가했다.

### Verification

- `npm test`(services/cli): quote-inspect 서명자 복구 round-trip(valid + tampered) +
reason-decode 회귀(check가 쓰는 recipe-aware per-element code) 스모크, 네트워크 불필요.
- Live walkthrough(`scripts/e2e-anvil.sh --keep`, fresh account 4): `check`(미attested →
A-02/A-03/A-04/C-01 FAIL + rejected, exit 1) → `investor-setup`+`kyc` → `check`(전 PASS,
ALLOWED) → `faucet` → `buy`(+100 RWA) → `sell 40`(RWA -40, QUOTE +40) → `balances`
전/후 → `snapshot`(0x11) → `attest jurisdiction ZZ` → `check`(정확히 A-02 하나 FAIL,
exit 1) → `restore 0x11` → `check`(ALLOWED) → `watch --from 0`(세션 이벤트 디코딩 재생,
라이브 MakerApprovalSet tail) → `rfq-quote` → `quote-inspect`(전 PASS) → 파일 서명
변조 → `quote-inspect`(signature FAIL, exit 1).
- `forge test --offline` 238/238 유지(Solidity 변경 없음).

### State

passing

### Notes

- runbook: `services/cli/README.md`(CLI v2 명령/주의), `docs/demo.md`.
- `check`는 엔진이 direction-aware가 아니라는 점을 도움말·표기로 명시(asset-side 라벨).
- `snapshot`/`restore`는 anvil 전용; `restore`는 이후 스냅샷을 무효화(문서화).
- Non-goals: CLI-001과 동일(프로덕션 key 관리, out/ ABI 커플링, 2차 web3 라이브러리).
65 changes: 35 additions & 30 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ source of truth로 사용한다.
라이브러리 재사용, reason-table 자동 디코딩. src/ 변경 없음(신규 Solidity는 KYC
forge 스크립트 하나). smoke + fresh-account live walkthrough로 검증. README는
`services/cli/README.md`.
- `CLI-002 — corner-store CLI v2`(`services/cli/**` + 문서만, 제품 코드/스크립트 변경
없음): preflight `check`(per-element + engine verdict, asset-side 라벨, rejected면
exit 1), AMM `sell`, `balances`, 이벤트 tail `watch`, `faucet`, anvil
`snapshot`/`restore`, `quote-inspect`(서명자 복구/만료/nonce·승인, 실패 시 exit 1).
reason 디코딩·EIP-712 복구는 기존 lib 재사용. smoke(quote-inspect valid+tampered) +
full live walkthrough로 검증. `forge test --offline` 238/238 유지.

## Blocked

Expand All @@ -67,40 +73,39 @@ source of truth로 사용한다.

## Last Session Summary

- `CLI-001` (corner-store Reference CLI)을 landing했다. src/ 제품 코드 변경 없이
`services/cli/` TypeScript CLI + `script/KycInvestor.s.sol` 하나로 live 노드
인터랙티브 클라이언트를 구현했다.
- `CLI-002` (corner-store CLI v2)를 landing했다. src/·script/ 변경 없이 `services/cli`에
명령 7개(check/sell/balances/watch/faucet/snapshot/restore/quote-inspect)를 더했다.
- 변경한 파일:
- CLI: `services/cli/`(package.json/tsconfig, `src/`: index/config/abi/reason/
elements/rfq/util/commands, `test/smoke.ts`, README.md)
- script: `script/KycInvestor.s.sol`(이미 배포된 ERC-3643 스택에 re-bind해 신규
투자자 identity+KYC claim, shared `TREXCore` 재사용)
- config: `.gitignore`(`services/cli/dist`,`node_modules`)
- docs/bookkeeping: `docs/demo.md`("CLI로 직접 해보기"), `FEATURES.md`(CLI-001),
`PROGRESS.md`
- CLI: `services/cli/src/`(abi/config/rfq/commands/index), `services/cli/test/smoke.ts`,
`services/cli/README.md`
- docs/bookkeeping: `docs/demo.md`, `FEATURES.md`(CLI-002), `PROGRESS.md`
- 설계 요점:
- chain 상호작용은 ethers(services/rfq에는 web3 라이브러리가 없어 CLI가 유일 도입).
EIP-712 quote는 services/rfq `RFQQuoteService`를 ethers wallet TypedDataSigner로
감싸 재사용. ABI는 `src/abi.ts` hand-written fragment(out/ 비의존).
- reason 디코딩 테이블은 `(recipe∈{1,2,7})×(11 element)×code1` + `encode(0,"POLICY",
status)`를 오프라인 사전계산; `cast keccak` ground-truth로 smoke에서 대조.
- admin 명령은 operator(account 0), buy는 buyer로 signer 기본값을 분기. 동일 명령
내 연속 tx는 ethers `NonceManager`로 RPC pending-nonce 레이스 방지.
- `kyc`는 forge cwd를 repo root로 설정하고 artifact를 root-상대 경로로 전달해
fs_permissions/스크립트 경로를 만족. `investor-setup`은 C-01 acquisition source를
t=1로 seed(recipe 1이 C-01 요구).
- `check`는 active manifest의 recipe → `requiredElements()` → element `check()`를
eth_call로 돌려 per-element PASS/FAIL을 만들고 `engine.evaluate`(view)로 verdict를
낸다. 엔진이 `ctx.buyer`만 스크리닝하므로 subject 무시 원소(B-01/B-02/E-01)를
`[asset-side]`로 표기. FAIL reason은 recipe-aware code로 디코딩(엔진이 낼 코드와 동일).
- `sell`은 `SwapFlow.t.sol::test_sell_shaped_success`의 컨텍스트(ctx.buyer=매도자,
tokenIn=RWA/tokenOut=QUOTE, venueData=zeroForOne=false)를 그대로 미러링.
- `watch`는 `eth_getLogs`(address+topic0 OR 필터) 폴링. reasonCode는 reason-table →
recipe-0 monitoring-flag(`encode(0,elementId,1)`, SurveillanceFlag) → bytes32 라벨
순으로 디코딩; ManifestStatusChanged status·SurveillanceFlag elementId도 사람이 읽게.
- EIP-712 복구는 services/rfq lib의 `domain`+`RFQ_QUOTE_TYPES`로 `verifyTypedData`
(타입 문자열 재선언 없음). ABI는 계속 hand-written fragment(out/ 비의존).
- 실행한 명령:
- `npm run build` / `npm test`(services/cli)
- `scripts/e2e-anvil.sh --keep` + 전체 CLI v2 walkthrough(account 4)
- `forge test --offline`
- `anvil` + `forge script DeployStack` + 전체 CLI walkthrough(account 4)
- 통과한 검증:
- `services/cli` smoke ok(reason ground-truth 대조 + quote round-trip).
- live walkthrough: onboard→investor-setup→kyc→buy PASS(+100), jurisdiction ZZ→
buy FAIL(A-02 decoded), suspend→buy FAIL(POLICY SUSPENDED), resume→buy PASS,
rfq-quote→rfq buy PASS(+200), maker revoke→rfq buy FAIL(RFQMakerNotApproved).
전 실패 경로 non-zero 종료.
- `forge test --offline` 238/238 유지(KycInvestor 추가 후에도 green).
- smoke ok: quote-inspect 서명자 복구(valid=maker / tampered≠maker) + reason 회귀.
- live walkthrough: fresh `check`(A-02/A-03/A-04/C-01 FAIL, exit 1) → setup+kyc →
`check`(전 PASS) → faucet → buy(+100) → sell 40(RWA-40/QUOTE+40) → balances 전후 →
snapshot(0x11) → attest ZZ → `check`(A-02 하나 FAIL, exit 1) → restore → `check`(green)
→ watch --from 0(세션 이벤트 디코딩 재생) → rfq-quote → quote-inspect(PASS) → 변조 →
quote-inspect(signature FAIL, exit 1).
- `forge test --offline` 238/238 유지(Solidity 변경 없음).
- 남은 리스크:
- `kyc`는 repo root 실행 전제(문서화). anvil 외 chainId/네트워크는 미검증(31337 고정).
- buy amount는 ether 단위 파싱(18 decimals) 가정; 비-18-decimals 토큰은 미지원.
- 나머지 스택 리스크는 E2E-001 세션의 것과 동일(MockPool AMM, C-01 mock source 등).
- manifest는 데모가 recipe 7(Reg D + Surveillance, 10 원소)로 onboarding한 상태라
`check`가 10개 원소를 매긴다(recipe 1이면 9개). 재배포 시 recipe에 따라 달라짐.
- `snapshot`/`restore`는 anvil 전용. 셸에서 id 캡처 시 `snapshot id:` 라인만 파싱할 것
(도움말의 "snapshot ids" 문구가 느슨한 grep에 걸릴 수 있음).
- amount는 ether 단위(18 decimals) 가정. 나머지 스택 리스크는 CLI-001/E2E-001과 동일.
10 changes: 10 additions & 0 deletions docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ MOCK / illustrative (documented seams):
`investor-setup` → `kyc` → `buy` → 실패 경로(jurisdiction flip / manifest suspend /
maker revoke, 각각 reason-code 디코딩) 순으로 직접 몰아볼 수 있다.

CLI v2(CLI-002)는 preflight·거래·관측 명령을 더한다: 거래 없이 per-element
컴플라이언스를 미리 확인하는 `check <buyer>`(엔진 verdict 포함, 거부 시 exit 1),
AMM 매도 방향 `sell <amountIn>`, 잔고/allowance 표 `balances`, 이벤트 실시간
tail `watch [--from <block>]`(Executed/RFQFilled/Manifest*/SurveillanceFlag 등
reason-code 디코딩), demo용 QUOTE 민팅 `faucet`, anvil `snapshot`/`restore <id>`,
서명 quote를 검증하는 `quote-inspect <file>`(서명자 복구·만료·on-chain nonce/승인
상태, 실패 시 exit 1). 예: `check`로 fresh 계정의 FAIL 원소들을 본 뒤
`investor-setup`+`kyc`로 green을 만들고, `snapshot` → `attest jurisdiction ZZ` →
`check`(정확히 A-02 하나 FAIL) → `restore`로 원복하는 흐름.

설치/실행법과 전체 walkthrough 레시피는 `services/cli/README.md`를 참고한다.

## Related
Expand Down
21 changes: 20 additions & 1 deletion services/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# corner-store CLI (CLI-001)
# corner-store CLI (CLI-001, CLI-002)

An interactive TypeScript reference client for driving the whole Corner Store
compliance-gated DEX stack against a live node from the terminal — onboarding,
Expand Down Expand Up @@ -47,6 +47,25 @@ corner-store maker <approve|revoke> <addr>
corner-store reason <bytes32> [--json]
```

CLI v2 (CLI-002) adds preflight, the sell direction, and observability:

```
corner-store check <buyer> [--venue amm|rfq] [--amount <n>] [--json] # per-element compliance preflight (no trade) + engine verdict; exit 1 if rejected
corner-store sell <amountIn> [--min <amountOut>] # AMM sell (tokenIn=RWA, tokenOut=QUOTE); defaults to investor account 1
corner-store balances [addr...] [--json] # RWA/QUOTE balances + adapter allowances (default: the 5 well-known roles)
corner-store watch [--from <block>] # live event tail (Executed/RFQFilled/RFQQuoteCancelled/MakerApprovalSet/Manifest*/SurveillanceFlag); Ctrl-C to stop
corner-store faucet <addr> <amount> # mint QUOTE (MockERC20.mint is permissionless — demo-only)
corner-store snapshot # anvil evm_snapshot -> prints id
corner-store restore <id> # anvil evm_revert (invalidates later snapshots)
corner-store quote-inspect <file> [--json] # decode a signed RFQ quote: recover signer, expiry, on-chain nonce/approval; exit 1 on any failed check
```

`check` screens `ctx.buyer` (the engine is not direction-aware); asset-side
elements (B-01/B-02/E-01) ignore the subject and are labelled as such so a
per-buyer FAIL on them is not misread. `check`/`quote-inspect` exit non-zero when
the verdict is rejected / any check fails, so they compose in scripts.
`snapshot`/`restore` require an anvil-style RPC and error clearly otherwise.

Amounts are given in ether units (18 decimals). Failed transactions exit
non-zero and print the decoded revert; any `ComplianceRejected(bytes32)` is
auto-decoded against the precomputed reason table (`reason` also decodes a code
Expand Down
28 changes: 28 additions & 0 deletions services/cli/src/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,34 @@ export const ELEMENT_REGISTRY_ABI = [
"function elementOf(bytes32 elementId) view returns (address)"
];

// RecipeRegistry.recipeOf(id) -> recipe address; the recipe exposes its required
// element id list (IRecipe.requiredElements). Used by `check` to enumerate the
// active manifest's per-element preflight set.
export const RECIPE_REGISTRY_ABI = [
"function recipeOf(uint16 recipeId) view returns (address)"
];
export const RECIPE_ABI = [
"function requiredElements() view returns (bytes32[])"
];

// ComplianceEngine.evaluate(ctx) is a VIEW returning the full ComplianceDecision
// (src/types/ComplianceTypes.sol). `check` calls it for the overall verdict.
export const ENGINE_ABI = [
"function evaluate(tuple(address initiator,address buyer,address seller,address tokenIn,address tokenOut,uint256 amountIn,uint256 amountOut,uint8 venueType,address venue,uint8 flowType,bool sellerIsAffiliate) ctx) view returns (tuple(bool allowed,bytes32 policyId,uint64 policyVersion,uint64 validUntil,uint256 maxAmount,uint256 allowedVenueTypes,bytes32 allowedVenuesHash,bytes32 reasonCode,bytes32 reliedClaims,bytes32 decisionHash))"
];

// Event fragments for `watch` (src/libraries/Events.sol + RFQAdapter.sol). Only
// the seven the tail decodes; topic0 hashes are derived from these at runtime.
export const EVENTS_ABI = [
"event Executed(bytes32 indexed executionId, address indexed venue, uint256 amountOut)",
"event RFQFilled(bytes32 indexed quoteHash, address indexed maker, address indexed taker, uint256 amountIn, uint256 amountOut)",
"event RFQQuoteCancelled(address indexed maker, uint256 indexed nonce)",
"event MakerApprovalSet(address indexed maker, bool approved)",
"event ManifestRegistered(address indexed token, uint16 issuanceRecipeId, address declaredBy)",
"event ManifestStatusChanged(address indexed token, uint8 status, bytes32 reasonCode)",
"event SurveillanceFlag(bytes32 indexed elementId, address indexed subject, bytes32 reasonCode)"
];

// IComplianceElement.check(user, counterparty, asset, amount, context).
export const ELEMENT_ABI = [
"function check(address user, address counterparty, address asset, uint256 amount, bytes context) view returns (bool passed, bytes32 reasonCode)"
Expand Down
Loading
Loading