Skip to content

Commit 4252555

Browse files
committed
feat: cip-conformance
Signed-off-by: Marc Juchli <marc.juchli@digitalasset.com>
1 parent 5c0fd6f commit 4252555

16 files changed

Lines changed: 790 additions & 0 deletions

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"script:test:examples": "tsx ./scripts/src/test-example-scripts.ts",
3737
"script:test:examples-stress": "tsx ./scripts/src/test-examples-scripts-under-stress.ts",
3838
"script:test:stress-scripts": "tsx ./scripts/src/test-stress-scripts.ts",
39+
"script:conformance:run": "yarn workspace @canton-network/cip103-conformance run run",
40+
"script:conformance:validate-artifact": "yarn workspace @canton-network/cip103-conformance run validate-artifact",
41+
"script:conformance:export-badge": "yarn workspace @canton-network/cip103-conformance run export-badge",
3942
"script:release": "tsx ./scripts/src/release.ts",
4043
"script:retag": "tsx ./scripts/src/retag.ts",
4144
"script:flat-pack": "tsx ./scripts/src/flat-pack.ts",
@@ -53,6 +56,7 @@
5356
"example-scripts",
5457
"sdk/**",
5558
"sdk-support/**",
59+
"tools/**",
5660
"scripts",
5761
"mock-oauth2",
5862
"wallet-gateway/**"

tools/cip103-conformance/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# CIP-103 Conformance CLI
2+
3+
Self-serve conformance checks for wallet providers against CIP-103 sync/async profiles.
4+
The CLI is built with `commander` and provides strict option validation.
5+
6+
## Commands
7+
8+
- `conformance-cli run --profile sync|async --provider-config <file>`
9+
- `conformance-cli validate-artifact --artifact <file> [--public-key <pem>] [--require-signature]`
10+
- `conformance-cli export-badge --artifact <file> [--out <file>]`
11+
12+
## Example Provider Config
13+
14+
```json
15+
{
16+
"name": "Example Wallet Provider",
17+
"version": "1.2.3",
18+
"endpoint": "http://localhost:8081/json-rpc",
19+
"timeoutMs": 10000
20+
}
21+
```
22+
23+
## Example Provider Config (Browser Extension Wallet)
24+
25+
Current implementation note: the conformance runner executes JSON-RPC over HTTP.
26+
For browser extension wallets, use a local bridge endpoint that exposes the extension methods over JSON-RPC.
27+
28+
```json
29+
{
30+
"name": "Example Browser Extension Wallet",
31+
"version": "1.0.0",
32+
"endpoint": "http://127.0.0.1:12481/json-rpc",
33+
"timeoutMs": 10000,
34+
"headers": {
35+
"x-provider-kind": "browser-extension"
36+
},
37+
"extensionId": "abcdefghijklmnoabcdefghijklmn",
38+
"injectedNamespace": "window.canton"
39+
}
40+
```
41+
42+
See `provider.config.browser-extension.example.json` for a copy-ready template.
43+
44+
## Profile Mapping
45+
46+
- `sync` -> `api-specs/openrpc-dapp-api.json`
47+
- `async` -> `api-specs/openrpc-dapp-remote-api.json`
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@canton-network/cip103-conformance",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"description": "Self-serve CIP-103 conformance CLI for sync/async wallet provider profiles.",
6+
"license": "Apache-2.0",
7+
"author": "Digital Asset",
8+
"main": "./dist/index.cjs",
9+
"module": "./dist/index.js",
10+
"types": "./dist/index.d.ts",
11+
"bin": {
12+
"conformance-cli": "./dist/cli.js"
13+
},
14+
"exports": {
15+
".": {
16+
"types": "./dist/index.d.ts",
17+
"import": "./dist/index.js",
18+
"require": "./dist/index.cjs",
19+
"default": "./dist/index.js"
20+
}
21+
},
22+
"scripts": {
23+
"build": "tsup && tsc -p tsconfig.types.json",
24+
"dev": "tsup --watch --onSuccess \"tsc -p tsconfig.types.json\"",
25+
"clean": "tsc -b --clean; rm -rf dist",
26+
"run": "tsx src/cli.ts"
27+
},
28+
"dependencies": {
29+
"commander": "^14.0.3",
30+
"zod": "^4.3.6"
31+
},
32+
"devDependencies": {
33+
"@types/node": "^25.3.3",
34+
"tsup": "^8.5.1",
35+
"tsx": "^4.21.0",
36+
"typescript": "^5.9.3"
37+
},
38+
"files": [
39+
"dist/**"
40+
],
41+
"publishConfig": {
42+
"access": "public"
43+
},
44+
"repository": {
45+
"type": "git",
46+
"url": "git+https://github.com/hyperledger-labs/splice-wallet-kernel.git",
47+
"directory": "tools/cip103-conformance"
48+
}
49+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "Example Browser Extension Wallet",
3+
"version": "1.0.0",
4+
"endpoint": "http://127.0.0.1:12481/json-rpc",
5+
"timeoutMs": 10000,
6+
"headers": {
7+
"x-provider-kind": "browser-extension"
8+
},
9+
"extensionId": "abcdefghijklmnoabcdefghijklmn",
10+
"injectedNamespace": "window.canton"
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "Example Wallet Provider",
3+
"version": "0.0.0",
4+
"endpoint": "http://localhost:8081/json-rpc",
5+
"timeoutMs": 10000
6+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { createPrivateKey, createPublicKey, sign, verify } from 'node:crypto'
5+
import { readFile, writeFile } from 'node:fs/promises'
6+
import { dirname, resolve } from 'node:path'
7+
import { mkdir } from 'node:fs/promises'
8+
import { ArtifactSchema, type Artifact } from './schemas'
9+
10+
function canonicalize(value: unknown): string {
11+
if (Array.isArray(value)) {
12+
return `[${value.map(canonicalize).join(',')}]`
13+
}
14+
if (value && typeof value === 'object') {
15+
const entries = Object.entries(value as Record<string, unknown>).sort(
16+
([a], [b]) => a.localeCompare(b)
17+
)
18+
return `{${entries
19+
.map(([key, val]) => `${JSON.stringify(key)}:${canonicalize(val)}`)
20+
.join(',')}}`
21+
}
22+
return JSON.stringify(value)
23+
}
24+
25+
function toSignPayload(artifact: Artifact): Buffer {
26+
const unsigned = { ...artifact, signature: undefined }
27+
return Buffer.from(canonicalize(unsigned), 'utf8')
28+
}
29+
30+
export async function writeArtifact(
31+
path: string,
32+
artifact: Artifact
33+
): Promise<void> {
34+
const absolutePath = resolve(process.cwd(), path)
35+
await mkdir(dirname(absolutePath), { recursive: true })
36+
await writeFile(
37+
absolutePath,
38+
`${JSON.stringify(artifact, null, 2)}\n`,
39+
'utf8'
40+
)
41+
}
42+
43+
export async function readArtifact(path: string): Promise<Artifact> {
44+
const absolutePath = resolve(process.cwd(), path)
45+
const raw = await readFile(absolutePath, 'utf8')
46+
return ArtifactSchema.parse(JSON.parse(raw))
47+
}
48+
49+
export async function signArtifact(
50+
artifact: Artifact,
51+
privateKeyPath: string,
52+
keyId?: string
53+
): Promise<Artifact> {
54+
const rawKey = await readFile(
55+
resolve(process.cwd(), privateKeyPath),
56+
'utf8'
57+
)
58+
const privateKey = createPrivateKey(rawKey)
59+
const signature = sign(null, toSignPayload(artifact), privateKey).toString(
60+
'base64'
61+
)
62+
return {
63+
...artifact,
64+
signature: {
65+
algorithm: 'ed25519',
66+
value: signature,
67+
keyId,
68+
},
69+
}
70+
}
71+
72+
export async function verifyArtifactSignature(
73+
artifact: Artifact,
74+
publicKeyPath: string
75+
): Promise<boolean> {
76+
if (!artifact.signature) {
77+
return false
78+
}
79+
const rawKey = await readFile(resolve(process.cwd(), publicKeyPath), 'utf8')
80+
const publicKey = createPublicKey(rawKey)
81+
return verify(
82+
null,
83+
toSignPayload(artifact),
84+
publicKey,
85+
Buffer.from(artifact.signature.value, 'base64')
86+
)
87+
}
88+
89+
export function toBadgeData(artifact: Artifact): Record<string, unknown> {
90+
return {
91+
schemaVersion: 1,
92+
label: `cip-103 ${artifact.profile}`,
93+
message: artifact.summary.status,
94+
color: artifact.summary.status === 'pass' ? 'green' : 'red',
95+
}
96+
}

0 commit comments

Comments
 (0)