@helios-lang/contract-utils 0.3.22 → 0.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,16 @@ Credentials are stored in a versioned `debugger.json` file:
26
26
 
27
27
  Set `HELIOS_CONFIG_HOME` to override the directory. Unix directories/files use modes 700/600. Windows uses a current-user-only ACL. The JSON has `{version:1,selected,profiles}`, where `selected` is a project ID and `profiles` maps project IDs to `{id,name,created_at,apiKey,wallet,endpoint}`. Names need not be unique. Re-running login refreshes that wallet's snapshot and removes revoked projects while preserving other wallets. It keeps the selected project when available and otherwise selects the oldest returned project. No signatures, browser session cookies, or temporary login tokens are persisted.
28
28
 
29
- API keys in this file are secrets: do not commit or share it. They authorize both capture uploads and reads for their project. Login never prints key values. The console's ordinary metadata endpoints do not return them; key recovery uses an explicitly approved, short-lived CLI request.
29
+ Project API keys can be shared with collaborators. They authorize capture uploads and reads for their project. The project page in the Console always displays its key in a yellow box.
30
+
31
+ To import a key from another project owner without wallet login:
32
+
33
+ ```sh
34
+ helios import-key hdbg_<key-from-project-page>
35
+ helios compile --project "Shared project" --help
36
+ ```
37
+
38
+ Import validates the key, resolves the project name automatically, and merges it into the same configuration used by compilation and VS Code. Reimporting updates that project without duplicating it or removing other projects. Wallet login preserves imported projects belonging to other wallets.
30
39
 
31
40
  For development, run `pnpm test`. `node scripts/check-global-cli.mjs` packs the package, installs it into an isolated npm global prefix, and checks executable discovery and equivalent compilation. The CLI portability workflow runs these checks on Windows, macOS, and Linux with Node 22 and 24. Publish the package only after the website and Worker with CLI-login support have been deployed.
32
41
 
@@ -46,3 +55,9 @@ Generated JS/TS exports `$debugger: {projectId,name,apiKey,endpoint,sources}` an
46
55
  With the corresponding tx-utils release and ledger evaluation-observer support, `spendWithRedeemer()`, minting/staking methods, and attached programs automatically deliver failed builds to that project. No explicit `makeDebuggerService()` call is needed. `makeTxBuilder({isMainnet, debugger:false})` disables automatic capture; an explicit debugger service overrides bundle configuration. Unannotated bundles retain their existing behavior.
47
56
 
48
57
  The API key is intentionally embedded in generated bundles, including browser bundles, and grants upload **and read** access to that project's feed. Use project-specific keys appropriate for that distribution. The key is sent in the authorization header, not in capture payloads. Omitting `--project` embeds no debugger credentials.
58
+
59
+ ### Offline wallet generation
60
+
61
+ `helios wallet create --network preprod` prints JSON containing a cryptographically random 24-word recovery phrase and its base address. It does not contact a service. Supported networks are `preprod` (default), `preview`, and `mainnet`.
62
+
63
+ Use `helios wallet create --network preprod --out wallet.json` to write a new private file instead of printing the phrase. Existing files are never overwritten. The output phrase can be pasted into a demo’s ignored `.env.local` file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helios-lang/contract-utils",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "Convenience and type-safety utilities for using Helios validators from within Typescript",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -42,6 +42,7 @@
42
42
  "@helios-lang/compiler-utils": "^0.5.15",
43
43
  "@helios-lang/crypto": "0.2.3",
44
44
  "@helios-lang/ledger": "^0.7.8",
45
+ "@helios-lang/tx-utils": "^0.6.41",
45
46
  "@helios-lang/type-utils": "^0.3.0",
46
47
  "@helios-lang/uplc": "^0.7.15",
47
48
  "open": "^10.2.0"
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto"
13
13
  import { execFile } from "node:child_process"
14
14
  import { promisify } from "node:util"
15
15
  const exec = promisify(execFile)
16
- async function windowsAcl(path, directory, set = false) {
16
+ export async function windowsAcl(path, directory, set = false) {
17
17
  // Pass paths through the environment, never interpolate them into PowerShell.
18
18
  const script = `
19
19
  $ErrorActionPreference = 'Stop'
@@ -102,11 +102,47 @@ export async function installProjects(
102
102
  endpoint,
103
103
  path = configPath()
104
104
  ) {
105
+ if (!/^[a-f0-9]{56}$/.test(wallet))
106
+ throw new Error("Invalid wallet response")
107
+ validateProjects(projects)
108
+ const data = await readConfig(path)
109
+ for (const [id, profile] of Object.entries(data.profiles))
110
+ if (profile.wallet === wallet && profile.endpoint === endpoint)
111
+ delete data.profiles[id]
112
+ for (const project of projects)
113
+ data.profiles[project.id] = { ...project, wallet, endpoint }
114
+ if (!data.profiles[data.selected])
115
+ data.selected = [...projects].sort(
116
+ (a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id)
117
+ )[0].id
118
+ return writeConfig(data, path)
119
+ }
120
+ export async function installSharedProject(
121
+ project,
122
+ endpoint,
123
+ path = configPath()
124
+ ) {
125
+ validateProjects([project])
126
+ const data = await readConfig(path)
127
+ const previous = data.profiles[project.id]
128
+ if (previous && previous.endpoint !== endpoint)
129
+ throw new Error(
130
+ "Project ID already exists for another debugger service"
131
+ )
105
132
  if (
106
- !/^[a-f0-9]{56}$/.test(wallet) ||
107
- !Array.isArray(projects) ||
108
- !projects.length
133
+ previous &&
134
+ previous.apiKey === project.apiKey &&
135
+ previous.name === project.name &&
136
+ previous.created_at === project.created_at
109
137
  )
138
+ return { path, alreadyExists: true }
139
+ data.profiles[project.id] = { ...previous, ...project, endpoint }
140
+ if (!data.profiles[data.selected]) data.selected = project.id
141
+ await writeConfig(data, path)
142
+ return { path, alreadyExists: false }
143
+ }
144
+ function validateProjects(projects) {
145
+ if (!Array.isArray(projects) || !projects.length)
110
146
  throw new Error("Invalid or empty project response")
111
147
  const ids = new Set()
112
148
  for (const p of projects) {
@@ -122,16 +158,8 @@ export async function installProjects(
122
158
  throw new Error("Invalid project response")
123
159
  ids.add(p.id)
124
160
  }
125
- const data = await readConfig(path)
126
- for (const [id, profile] of Object.entries(data.profiles))
127
- if (profile.wallet === wallet && profile.endpoint === endpoint)
128
- delete data.profiles[id]
129
- for (const project of projects)
130
- data.profiles[project.id] = { ...project, wallet, endpoint }
131
- if (!data.profiles[data.selected])
132
- data.selected = [...projects].sort(
133
- (a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id)
134
- )[0].id
161
+ }
162
+ async function writeConfig(data, path) {
135
163
  const dir = dirname(path)
136
164
  const existed = await inspect(dir, true)
137
165
  await mkdir(dir, { recursive: true, mode: 0o700 })
@@ -0,0 +1,56 @@
1
+ import { installSharedProject } from "./config.mjs"
2
+
3
+ export async function importKey(
4
+ apiKey,
5
+ {
6
+ endpoint = "https://debugger.helios-lang.io",
7
+ fetchImpl = fetch,
8
+ save = installSharedProject,
9
+ log = console.log
10
+ } = {}
11
+ ) {
12
+ if (typeof apiKey !== "string" || !/^hdbg_[a-f0-9]{64}$/.test(apiKey))
13
+ throw new Error(
14
+ "Invalid debugger API key. Copy the full key from the project's Console page."
15
+ )
16
+ const origin = new URL(endpoint)
17
+ if (
18
+ origin.protocol !== "https:" ||
19
+ origin.username ||
20
+ origin.password ||
21
+ origin.pathname !== "/" ||
22
+ origin.search ||
23
+ origin.hash
24
+ )
25
+ throw new Error("Debugger endpoint must be an HTTPS origin")
26
+ let response
27
+ try {
28
+ response = await fetchImpl(origin.origin + "/v1/project", {
29
+ headers: { Authorization: `Bearer ${apiKey}` },
30
+ redirect: "error",
31
+ signal: AbortSignal.timeout(10000)
32
+ })
33
+ } catch {
34
+ throw new Error(
35
+ "Could not reach the debugger service. Check your connection and try again."
36
+ )
37
+ }
38
+ if (response.status === 401)
39
+ throw new Error(
40
+ "This debugger API key is invalid or revoked. Ask the project owner for an active key."
41
+ )
42
+ if (!response.ok)
43
+ throw new Error(
44
+ `Debugger project lookup failed (HTTP ${response.status}). Try again later.`
45
+ )
46
+ const { id, name, created_at } = await response.json()
47
+ const { path, alreadyExists } = await save(
48
+ { id, name, created_at, apiKey },
49
+ origin.origin
50
+ )
51
+ if (alreadyExists) log("Key already exists")
52
+ else
53
+ log(`Imported project ${JSON.stringify(name)} into ${path}.
54
+ Compile with helios compile --project ${JSON.stringify(name)} (or hl2ts --project ${JSON.stringify(name)}).`)
55
+ return { id, name, created_at }
56
+ }
@@ -0,0 +1,14 @@
1
+ import { open, unlink } from "node:fs/promises"
2
+ import { windowsAcl } from "./config.mjs"
3
+ export async function writePrivateWallet(path, json) {
4
+ const file = await open(path, "wx", 0o600)
5
+ try {
6
+ if (process.platform === "win32") await windowsAcl(path, false, true)
7
+ await file.writeFile(json, "utf8")
8
+ } catch (error) {
9
+ await file.close()
10
+ await unlink(path)
11
+ throw error
12
+ }
13
+ await file.close()
14
+ }
@@ -0,0 +1,51 @@
1
+ import { randomBytes } from "node:crypto"
2
+ import { parseArgs } from "node:util"
3
+ import { makeRootPrivateKey } from "@helios-lang/tx-utils"
4
+ import { makeShelleyAddress } from "@helios-lang/ledger"
5
+
6
+ export function createWallet(network = "preprod") {
7
+ if (!["preprod", "preview", "mainnet"].includes(network))
8
+ throw new Error("Network must be preprod, preview, or mainnet")
9
+ const key = makeRootPrivateKey([...randomBytes(32)])
10
+ const address = makeShelleyAddress(
11
+ network === "mainnet",
12
+ key.deriveSpendingKey().derivePubKey().hash(),
13
+ key.deriveStakingKey().derivePubKey().hash()
14
+ )
15
+ return {
16
+ network,
17
+ phrase: key.toPhrase().join(" "),
18
+ address: address.toBech32()
19
+ }
20
+ }
21
+ export async function walletCommand(args) {
22
+ const [command, ...rest] = args
23
+ if (["--help", "-h", "help"].includes(command)) {
24
+ console.log(
25
+ "helios wallet create [--network preprod|preview|mainnet] [--out wallet.json]\nGenerates a 24-word recovery phrase and address offline. Without --out, prints the secret phrase as JSON. --out creates a new owner-only file and never overwrites an existing file."
26
+ )
27
+ return
28
+ }
29
+ if (command !== "create")
30
+ throw new Error(
31
+ "Usage: helios wallet create [--network preprod|preview|mainnet] [--out wallet.json]"
32
+ )
33
+ const { values } = parseArgs({
34
+ args: rest,
35
+ options: {
36
+ network: { type: "string", default: "preprod" },
37
+ out: { type: "string" }
38
+ }
39
+ })
40
+ const wallet = createWallet(values.network)
41
+ const json = JSON.stringify(wallet, null, 2) + "\n"
42
+ if (values.out !== undefined) {
43
+ if (!values.out.trim()) throw new Error("--out requires a file path")
44
+ // Reuse credential-file ACL protection on Windows, before storing secrets.
45
+ const { writePrivateWallet } = await import("./wallet-file.mjs")
46
+ await writePrivateWallet(values.out, json)
47
+ console.log(
48
+ `Created ${wallet.network} wallet: ${wallet.address}\nRecovery phrase saved in ${values.out}`
49
+ )
50
+ } else console.log(json)
51
+ }
package/src/helios.mjs CHANGED
@@ -7,6 +7,14 @@ try {
7
7
  await compile(
8
8
  args.map((arg) => (["--help", "-h"].includes(arg) ? "help" : arg))
9
9
  )
10
+ } else if (command === "wallet") {
11
+ const { walletCommand } = await import("./cli/wallet.mjs")
12
+ await walletCommand(args)
13
+ } else if (command === "import-key") {
14
+ if (args.length !== 1)
15
+ throw new Error("Usage: helios import-key <api-key>")
16
+ const { importKey } = await import("./cli/import-key.mjs")
17
+ await importKey(args[0])
10
18
  } else if (command === "login") {
11
19
  if (args.some((arg) => arg !== "--no-browser"))
12
20
  throw new Error("Usage: helios login [--no-browser]")
@@ -38,7 +46,7 @@ try {
38
46
  )
39
47
  } else if (!command || ["--help", "-h", "help"].includes(command)) {
40
48
  console.log(
41
- "Helios CLI\n\n helios compile [options] Compile contracts (same options as hl2ts)\n helios login [--no-browser] Install project API keys through the Console\n helios --version"
49
+ "Helios CLI\n\n helios compile [options] Compile contracts (same options as hl2ts)\n helios login [--no-browser] Install project API keys through the Console\n helios import-key <api-key> Install a shared project API key\n helios wallet create [--network preprod] [--out wallet.json] Generate a wallet offline\n helios --version"
42
50
  )
43
51
  } else throw new Error(`Unknown command: ${command}. Run helios --help.`)
44
52
  } catch (error) {
@@ -1,4 +1,9 @@
1
+ export function windowsAcl(path: any, directory: any, set?: boolean): Promise<void>;
1
2
  export function configPath(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, home?: string): string;
2
3
  export function readConfig(path?: string): Promise<any>;
3
- export function installProjects(wallet: any, projects: any, endpoint: any, path?: string): Promise<string>;
4
+ export function installProjects(wallet: any, projects: any, endpoint: any, path?: string): Promise<any>;
5
+ export function installSharedProject(project: any, endpoint: any, path?: string): Promise<{
6
+ path: string;
7
+ alreadyExists: boolean;
8
+ }>;
4
9
  //# sourceMappingURL=config.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","sourceRoot":"","sources":["../../src/cli/config.mjs"],"names":[],"mappings":"AAiDA,uGAcC;AAqBD,wDAaC;AACD,2GA2DC"}
1
+ {"version":3,"file":"config.d.mts","sourceRoot":"","sources":["../../src/cli/config.mjs"],"names":[],"mappings":"AAeA,oFAiCC;AACD,uGAcC;AAqBD,wDAaC;AACD,wGAoBC;AACD;;;GAuBC"}