diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 9e8a17c..6ec2500 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -113,7 +113,8 @@ jobs: PORT=4310 \ STUDIO_DATA_DIR="$data_dir" \ SILVERC_LATEST_BIN="$runtime/bin/silverc-latest" \ - SILVERC_PREVIOUS_BIN="$runtime/bin/silverc-cb34aa5" \ + SILVERC_PREVIOUS_BIN="$runtime/bin/silverc-6f9e078" \ + SILVERC_OLDER_BIN="$runtime/bin/silverc-cb34aa5" \ SILVERC_LEGACY_BIN="$runtime/bin/silverc-legacy" \ KASCOV_PREFLIGHT_BIN="$runtime/bin/kascov-preflight" \ "$node_bin" "$runtime/server/index.mjs" >"$log" 2>&1 & diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0e40b..79ad386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.2.9 — 2026-08-15 + +- Updated the default SHA-256-pinned official SilverScript compiler to `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`; retained `6f9e078`, `cb34aa5`, and `2a3961c` as isolated reproducibility profiles. +- Added compatibility findings for declaration shadowing, `.split()` result typing, immutable `for` iterators, and the 10,000-iteration compiler limit introduced by the latest hardening changes. +- Added KCC20 security triage for borrowed-receive paths, including minter-leader rejection, token-state invariants, KAS-value preservation, pre-signed outpoint invalidation, and co-spend consent review. +- Extended the atomic multi-Covenant builder with explicit fresh-lineage outputs authorized by a verified input Covenant ID; genesis bindings are populated by the Kaspa transaction library and recorded in the operation package. +- Aligned KCC721 metadata with the KCC-0021 draft image-integrity rule: HTTPS images require a committed SHA-256, while IPFS URIs remain content-addressed. +- Updated the TN10 x402 interoperability profile to `kaspa-x402@alpha.10` (`78f2ada`) and documented stable `kaspa-escrow-v2` lineage plus partial-claim, top-up, and refund semantics without promoting it to a deployable built-in template. +- Documented the community TN10 cross-Covenant fusion transaction as evidence only; unavailable source and reproducibility data remain an explicit admission blocker. +- Added a visible 0.2.9 capability hub and a real TN10 atomic-builder interface in the Operation Center; KCC20 scan, compiler, and KCC721 actions now open their actual workflows, while x402 is clearly labeled research-only. + +## 0.2.9 — 2026-08-15(中文) + +- 默认官方 SilverScript 编译器升级并固定到 `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`;隔离保留 `6f9e078`、`cb34aa5` 和 `2a3961c` 复现档案。 +- 增加最新编译器强化变更的兼容提示:声明名称遮蔽、`.split()` 结果类型、不可修改的 `for` 迭代变量,以及 10,000 次循环上限。 +- 增加 KCC20 安全初筛:检查借用接收路径的 minter leader 拒绝、Token 状态不变量、KAS value 保持、预签 outpoint 失效风险和共同输入授权语义。 +- 原子多 Covenant 构建器支持由已核验输入 Covenant ID 授权的新 lineage 输出;由 Kaspa 交易库填充 genesis binding,并将结果写入操作包。 +- KCC721 元数据采用 KCC-0021 Draft 图片完整性规则:HTTPS 图片必须提交 SHA-256,IPFS URI 继续依赖内容寻址。 +- TN10 x402 互操作档案升级到 `kaspa-x402@alpha.10`(`78f2ada`),记录稳定的 `kaspa-escrow-v2` lineage 与部分领取、追加和退款语义,但仍不伪装成可部署内置模板。 +- 将社区 TN10 跨 Covenant fusion 交易作为链上证据记录;缺少源码与可复现数据仍是正式准入阻断项。 +- 增加醒目的 0.2.9 功能入口区和操作中心 TN10 原子构建界面;KCC20 检查、编译器和 KCC721 按钮会进入真实操作流程,x402 明确标为仅研究、不可部署。 + ## 0.2.8 — 2026-08-10 - Updated the default SHA-256-pinned official SilverScript compiler to `6f9e078b1d8b5389212755183b592704de99fea5`; retained `cb34aa5` for Studio 0.2.7 reproducibility and `2a3961c` for older projects. diff --git a/README.md b/README.md index 71a11a3..82445a6 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 - 中英文界面;首次启动自动读取系统语言,并在系统语言既非中文也非英文时使用时区辅助判断。 - 用户手动切换语言后,本机选择优先于自动识别。 - 本地项目工作区,可创建、切换和删除未使用的工作。 -- 三编译器兼容档案:默认固定 `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5`,保留 Studio 0.2.7 使用的 `cb34aa5` 和 `2a3961c` 旧版用于复现;三个二进制都校验 SHA-256。 -- 内置破坏性变更扫描与安全迁移,识别 `entry`、`checkMsgSig`、`outpointTxId`、artifact `bytecode` 和显式标量 byte/int 转换;无法安全自动迁移的 signed/unsigned 语义、`.reverse()` 和位运算会要求人工审查。 +- 四编译器兼容档案:默认固定 `kaspanet/silverscript@14dce9a5ce8769cdfbd0c8965f8764fa9c325067`,隔离保留 Studio 0.2.8 的 `6f9e078`、Studio 0.2.7 的 `cb34aa5` 和 `2a3961c` 旧版用于复现;所有二进制都校验 SHA-256。 +- 内置破坏性变更扫描与安全迁移,覆盖 `entry`、`checkMsgSig`、`outpointTxId`、artifact `bytecode`、标量转换、声明遮蔽、`.split()` 类型、不可变循环变量和循环上限;无法安全自动迁移的语义会要求人工审查。 - 固定 Kascov 来源提交构建的本地交易预检引擎。 - 支持 OpenAI、Anthropic、Gemini、OpenRouter、Ollama 和 OpenAI-compatible 接口。 - AI API Key 使用 scrypt 派生密钥和 AES-256-GCM 加密保存在本机。 @@ -47,8 +47,10 @@ AI 只负责生成候选方案和辅助审查,不能解锁钱包、签名交 - 支持 `.ssinvite` 可携带操作包、跨设备顺序签名和外部 Covenant 交易包审查。 - 新操作包携带 canonical v1 Covenant Descriptor,绑定 CAIP-2 网络、程序哈希、Covenant ID、ABI、状态布局和授权主体;旧包仍可读取但会标记缺少描述符。 - 可替换 `CovenantStateSource` 会在原生 Covenant RPC、outpoint RPC 与 P2SH 地址索引之间回退,并重新验证 outpoint、Covenant ID、脚本和金额。 -- 通用 P2PK co-spend 授权只签指定普通钱包输入,并锁定整笔交易承诺;原子构建器支持 2–32 个不同 Covenant 输入。 +- 通用 P2PK co-spend 授权只签指定普通钱包输入,并锁定整笔交易承诺;原子构建器支持 2–32 个不同 Covenant 输入、同 lineage 延续和由已核验输入授权的新 lineage 输出。 +- KCC20 安全初筛覆盖借用接收、minter leader、Token 状态不变量、KAS value 保持、预签 outpoint 失效与共同输入授权边界。 - Kascov 是首选可视化和第二份报告来源,但不是签名、预检或广播的运行依赖。 +- 首页提供 0.2.9 功能入口;操作中心包含 TN10 原子多 Covenant JSON 构建与只读审查界面。研究档案会明确标记为不可部署。 ### 内置模板 @@ -224,7 +226,7 @@ OLLAMA_MODEL= 仅有 Covenant ID 或 cov hash 不足以签名。外部操作包必须携带待签交易、UTXO、redeem program、ABI、入口、参数、输出和签名槽信息。详见 [可携带 Covenant 操作包](docs/portable-covenant-package.md)。 -编译器升级、状态查询与原子授权接口见 [Studio 0.2 架构说明](docs/studio-0.2-architecture.md)。KCC721 包的来源、边界和禁止事项见 [TN10 Experimental KCC721](docs/kcc721-experimental.md)。 +编译器升级、状态查询与原子授权接口见 [Studio 0.2 架构说明](docs/studio-0.2-architecture.md)。KCC721 包的来源、边界和禁止事项见 [TN10 Experimental KCC721](docs/kcc721-experimental.md)。社区跨 Covenant 交易证据与新 lineage 构建格式见 [TN10 跨 Covenant Fusion 说明](docs/tn10-cross-covenant-fusion.md)。 Kaspa x402 的网络标识、操作包映射和正式模板准入条件见 [TN10 Experimental x402 档案](docs/x402-experimental-profile.md)。 ### 网络 @@ -293,8 +295,8 @@ AI is limited to candidate generation and review assistance. It cannot unlock wa - Chinese and English UI with automatic system-language detection and time-zone fallback. - A manual language choice always overrides future automatic detection. - Local project workspace with explicit create, switch, and delete actions. -- Three compiler profiles: the default is pinned to `kaspanet/silverscript@6f9e078b1d8b5389212755183b592704de99fea5`; Studio 0.2.7's `cb34aa5` and legacy `2a3961c` remain reproducible, with SHA-256 verification for all binaries. -- Built-in breaking-change detection and safe migration for `entry`, `checkMsgSig`, `outpointTxId`, artifact `bytecode`, and explicit scalar byte/int conversions; signedness, removed `.reverse()`, and bitwise typing changes require manual review. +- Four isolated compiler profiles: the default is pinned to `kaspanet/silverscript@14dce9a5ce8769cdfbd0c8965f8764fa9c325067`; Studio 0.2.8's `6f9e078`, Studio 0.2.7's `cb34aa5`, and legacy `2a3961c` remain reproducible, with SHA-256 verification for every binary. +- Built-in breaking-change detection and safe migration cover `entry`, `checkMsgSig`, `outpointTxId`, artifact `bytecode`, scalar conversions, declaration shadowing, `.split()` typing, immutable loop iterators, and bounded-loop limits; ambiguous semantic changes require manual review. - Pinned Kascov-derived local transaction preflight engine. - OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and OpenAI-compatible providers. - AES-256-GCM encrypted AI key vault with a scrypt-derived key. @@ -304,8 +306,10 @@ AI is limited to candidate generation and review assistance. It cannot unlock wa - Portable `.ssinvite` operation packages, sequential cross-device signing, and external covenant-package review. - Canonical v1 covenant descriptors bind each new package to its CAIP-2 network, program hash, covenant ID, ABI, state layout, and authorization principals; legacy packages remain readable with a visible missing-descriptor warning. - Replaceable `CovenantStateSource` fallback across native covenant RPC, outpoint RPC, and P2SH address indexing, with independent outpoint, covenant ID, script, and value verification. -- Generic isolated P2PK co-spend authorization plus an atomic builder for 2–32 distinct covenant inputs. +- Generic isolated P2PK co-spend authorization plus an atomic builder for 2–32 distinct Covenant inputs, same-lineage continuations, and fresh lineages authorized by verified input Covenant IDs. +- KCC20 security triage covers borrowed receive, minter leaders, token-state invariants, KAS-value preservation, pre-signed outpoint invalidation, and co-spend consent boundaries. - Kascov is the preferred visual and secondary-report layer, not a signing, preflight, or broadcast dependency. +- The home screen exposes the 0.2.9 workflows, and the Operation Center includes a TN10 atomic multi-Covenant JSON builder with read-only package review. Research profiles are visibly marked non-deployable. ### Built-in templates @@ -455,7 +459,7 @@ Never have multiple signers sign separate initial copies. Compare the transactio A covenant ID or cov hash alone is not a signing request. An external package must include the exact transaction, UTXOs, redeem program, ABI, entrypoint, arguments, outputs, and signature slots. See [Portable covenant packages](docs/portable-covenant-package.md). -See [Studio 0.2 architecture](docs/studio-0.2-architecture.md) for compiler upgrades, state sources, P2PK authorization, and atomic transaction APIs. See [TN10 Experimental KCC721](docs/kcc721-experimental.md) for provenance, boundaries, and prohibited release claims. +See [Studio 0.2 architecture](docs/studio-0.2-architecture.md) for compiler upgrades, state sources, P2PK authorization, and atomic transaction APIs. See [TN10 Experimental KCC721](docs/kcc721-experimental.md) for provenance, boundaries, and prohibited release claims. See [TN10 cross-Covenant fusion](docs/tn10-cross-covenant-fusion.md) for community transaction evidence and the fresh-lineage build format. See the [TN10 Experimental x402 profile](docs/x402-experimental-profile.md) for network identifiers, operation-package mapping, and executable-template admission gates. ### Networks diff --git a/config/compiler-profiles.json b/config/compiler-profiles.json index c13eda3..8dc2397 100644 --- a/config/compiler-profiles.json +++ b/config/compiler-profiles.json @@ -1,17 +1,37 @@ { - "defaultProfileId": "latest-6f9e078", + "defaultProfileId": "latest-14dce9a", "profiles": [ { - "id": "latest-6f9e078", - "label": "SilverScript latest (6f9e078)", - "upstreamCommit": "6f9e078b1d8b5389212755183b592704de99fea5", + "id": "latest-14dce9a", + "label": "SilverScript latest (14dce9a)", + "upstreamCommit": "14dce9a5ce8769cdfbd0c8965f8764fa9c325067", "binary": "bin/silverc-latest", "artifactBytecodeField": "bytecode", "syntaxGeneration": 2, "status": "experimental", "networkPolicy": "tn10-only", + "releasedAt": "2026-08-13", + "notes": [ + "Includes c1ed163 entrypoint, constant-cycle and bounded-loop hardening", + "Rejects declaration shadowing and attempts to override compiler builtins", + "Makes struct assignment atomic and struct-field lowering collision resistant", + "Fixes ternary lowering, struct append, bool array normalization and split result typing", + "Requires fixed byte arrays of at most eight bytes for int(byte[N]) casts", + "Keeps loop iteration variables immutable" + ] + }, + { + "id": "latest-6f9e078", + "label": "SilverScript Studio 0.2.8 (6f9e078)", + "upstreamCommit": "6f9e078b1d8b5389212755183b592704de99fea5", + "binary": "bin/silverc-6f9e078", + "artifactBytecodeField": "bytecode", + "syntaxGeneration": 2, + "status": "previous", + "networkPolicy": "tn10-only", "releasedAt": "2026-08-10", "notes": [ + "Retained to reproduce Studio 0.2.8 projects", "Uses entry syntax for public entrypoints", "Exposes transaction introspection as outpointTxId", "Uses checkMsgSig for arbitrary-message signatures", @@ -25,7 +45,7 @@ }, { "id": "latest-cb34aa5", - "label": "SilverScript previous (cb34aa5)", + "label": "SilverScript Studio 0.2.7 (cb34aa5)", "upstreamCommit": "cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "binary": "bin/silverc-cb34aa5", "artifactBytecodeField": "bytecode", @@ -60,7 +80,7 @@ "severity": "error", "introducedBy": "0f99803", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\bentrypoint\\s+function\\b", "messageZh": "公开入口语法已从 entrypoint function 改为 entry。", "messageEn": "Public entrypoint syntax changed from entrypoint function to entry.", @@ -71,7 +91,7 @@ "severity": "error", "introducedBy": "782a4d7", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\bcheckSigFromStack\\b", "messageZh": "任意消息签名内建函数已改名为 checkMsgSig。", "messageEn": "The arbitrary-message signature builtin was renamed to checkMsgSig.", @@ -82,7 +102,7 @@ "severity": "error", "introducedBy": "65421cf", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\.outpointTransactionHash\\b", "messageZh": "输入 outpoint 字段已改名为 outpointTxId。", "messageEn": "The input outpoint field was renamed to outpointTxId.", @@ -93,7 +113,7 @@ "severity": "error", "introducedBy": "6869e7d", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "\\.reverse\\s*\\(", "messageZh": ".reverse() 已删除,必须显式重写字节顺序逻辑并重新测试。", "messageEn": ".reverse() was removed; rewrite byte ordering explicitly and retest it.", @@ -104,7 +124,7 @@ "severity": "integration", "introducedBy": "4d88ded", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": null, "messageZh": "编译产物 JSON 字段由 script 改为 bytecode;外部工具必须兼容两个字段。", "messageEn": "The compiler artifact JSON field changed from script to bytecode; integrations must support both.", @@ -115,11 +135,55 @@ "severity": "manual-review", "introducedBy": "8b74812", "fromProfile": "legacy-2a3961c", - "toProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", "pattern": "(?:^|[^&|])(?:<<|>>|\\^|&|\\|)(?:[^&|]|$)", "messageZh": "位运算现在仅接受 bytes;请人工确认操作数类型。", "messageEn": "Bitwise operators now accept bytes only; review operand types manually.", "replacement": null + }, + { + "id": "declaration-shadowing-rejected", + "severity": "manual-review", + "introducedBy": "14dce9a", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": null, + "messageZh": "编译器现在禁止构造参数、字段、常量、函数参数和局部变量之间的名称遮蔽;请通过完整编译定位并重命名冲突声明。", + "messageEn": "The compiler now rejects name shadowing across constructor parameters, fields, constants, function parameters and locals; fully compile and rename conflicting declarations.", + "replacement": null + }, + { + "id": "split-result-typing", + "severity": "manual-review", + "introducedBy": "14dce9a", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": "\\.split\\s*\\(", + "messageZh": ".split() 结果现在会推断固定左侧长度;请复核显式类型、析构赋值和下游状态编码。", + "messageEn": ".split() now infers a fixed left-side length; review explicit types, tuple destructuring and downstream state encoding.", + "replacement": null + }, + { + "id": "for-iterator-immutable", + "severity": "error", + "introducedBy": "14dce9a", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": "for\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)[^\\n]*\\)[\\s\\S]{0,500}?\\b\\1\\s*(?:=|\\+=|-=|\\+\\+|--)", + "messageZh": "for 循环迭代变量现在不可修改;请使用独立局部变量表达循环内状态。", + "messageEn": "For-loop iteration variables are now immutable; use a separate local for mutable loop state.", + "replacement": null + }, + { + "id": "bounded-loop-limit", + "severity": "manual-review", + "introducedBy": "c1ed163", + "fromProfile": "latest-6f9e078", + "toProfile": "latest-14dce9a", + "pattern": "max_iterations\\s*=\\s*(?:1000[1-9]|100[1-9][0-9]|10[1-9][0-9]{2}|1[1-9][0-9]{3}|[2-9][0-9]{4,})", + "messageZh": "编译器将 max_iterations 限制为 10,000;请缩小循环边界并重新评估脚本单位。", + "messageEn": "The compiler caps max_iterations at 10,000; reduce the bound and reassess script units.", + "replacement": null } ] } diff --git a/docs/kcc721-experimental.md b/docs/kcc721-experimental.md index 7b65a3b..e373524 100644 --- a/docs/kcc721-experimental.md +++ b/docs/kcc721-experimental.md @@ -6,8 +6,9 @@ Studio 内置的 KCC721 包改编自 `KaspaHUB21/KCC721` v0.2 社区草案,保 本包只允许 `tn10`,风险等级为 `high-experimental`。普通单 Covenant 部署路径被明确禁用,因为 Collection/Ticket/NFT 创世需要专用的多合约 builder 正确计算模板片段、模板哈希、Covenant ID 和 output binding。当前完成的是: -- 四份源码在官方 `silverc@6f9e078` 下完整编译。 +- 四份源码在官方 `silverc@14dce9a` 下完整编译。 - 模板使用三步配置向导,不再要求用户手填元数据摘要。名称、描述、图片 URI、外部链接和属性会先规范化为确定性 JSON,再由前后端分别计算并核对 SHA-256。 +- 元数据采用 KCC-0021 Draft 的图片完整性规则:`https://` 图片必须附带 32-byte SHA-256,`ipfs://` 由 CID 提供内容寻址;KCC-0021 尚未合并,界面继续标为 Draft/Experimental。 - “新集合”明确标记为编译预览,内部使用不可部署的全零哨兵;只有“导入已有 TN10 集合”模式接受从真实创世输出核验的 Collection Covenant ID。 - NFT 所有者变更绑定独立 P2PK co-spend 输入。 - 原子 builder 支持 2–32 个不同 Covenant ID 的批量交易。 @@ -34,8 +35,9 @@ The bundled KCC721 pack is adapted from the community `KaspaHUB21/KCC721` v0.2 d The pack is restricted to `tn10` and marked `high-experimental`. Ordinary single-covenant deployment is explicitly blocked because Collection/Ticket/NFT genesis requires a dedicated multi-contract builder to calculate template segments, template hashes, covenant IDs, and output bindings correctly. The current implementation provides: -- Full compilation of all four sources with official `silverc@6f9e078`. +- Full compilation of all four sources with official `silverc@14dce9a`. - A three-step setup wizard that no longer asks users to type a metadata digest. Name, description, image URI, external URL, and attributes are canonicalized into deterministic JSON, then SHA-256 is independently recomputed by the client and server. +- Metadata follows the KCC-0021 Draft image-integrity rule: `https://` images require a 32-byte SHA-256 while `ipfs://` is content-addressed by its CID. KCC-0021 is still unmerged, so the UI remains Draft/Experimental. - A clearly labeled new-collection compile preview with a non-deployable internal all-zero sentinel. Only the existing-TN10-collection path accepts a Collection covenant ID verified from a real genesis output. - NFT ownership transitions bound to a separate P2PK co-spend input. - Atomic transactions across 2–32 distinct covenant IDs. diff --git a/docs/studio-0.2-architecture.md b/docs/studio-0.2-architecture.md index 6c8cc93..dab0667 100644 --- a/docs/studio-0.2-architecture.md +++ b/docs/studio-0.2-architecture.md @@ -4,7 +4,7 @@ ### 编译器兼容档案 -`config/compiler-profiles.json` 是可提交的兼容性清单,`config/compiler.json` 是本机生成的二进制路径、构建时间和 SHA-256 清单。默认档案固定官方 SilverScript `6f9e078b1d8b5389212755183b592704de99fea5`;`cb34aa5` 保留用于复现 Studio 0.2.7 项目,`2a3961c` 保留用于更早的旧版项目。 +`config/compiler-profiles.json` 是可提交的兼容性清单,`config/compiler.json` 是本机生成的二进制路径、构建时间和 SHA-256 清单。默认档案固定官方 SilverScript `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`;`6f9e078`、`cb34aa5` 和 `2a3961c` 隔离保留,用于复现 Studio 0.2.8、0.2.7 与更早项目。 升级检查会报告已知变化,并只自动替换无歧义的名称。`6f9e078` 要求标量 byte 转 int 时明确使用 `signed()` 或 `unsigned()`,运行时 int 转 byte 使用 `as byte`;这些语义不能自动猜测。`.reverse()` 删除、字节序、位运算类型和任何状态布局变化也必须人工审查。迁移后仍必须使用真实构造参数完整编译并进行对抗性交易测试。 @@ -54,7 +54,7 @@ Studio 新生成的 `.ssinvite` 为每个 Covenant 输入附带 canonical v1 des ### Compiler compatibility profiles -`config/compiler-profiles.json` is the committed compatibility catalog. The generated `config/compiler.json` records local binary paths, build times, and SHA-256 hashes. The default profile pins official SilverScript commit `6f9e078b1d8b5389212755183b592704de99fea5`; `cb34aa5` reproduces Studio 0.2.7 projects and `2a3961c` remains for older legacy projects. +`config/compiler-profiles.json` is the committed compatibility catalog. The generated `config/compiler.json` records local binary paths, build times, and SHA-256 hashes. The default profile pins official SilverScript commit `14dce9a5ce8769cdfbd0c8965f8764fa9c325067`; isolated `6f9e078`, `cb34aa5`, and `2a3961c` profiles reproduce Studio 0.2.8, 0.2.7, and older projects. Compatibility checks report known changes and automatically apply only unambiguous renames. Commit `6f9e078` requires explicit `signed()`/`unsigned()` conversion from scalar byte and `as byte` for checked runtime int conversion; Studio never guesses that meaning. Removed `.reverse()`, byte ordering, bitwise typing, and state-layout changes also require manual review. Every migration still requires a full compile with realistic constructor arguments and adversarial transaction tests. diff --git a/docs/tn10-cross-covenant-fusion.md b/docs/tn10-cross-covenant-fusion.md new file mode 100644 index 0000000..ad98eab --- /dev/null +++ b/docs/tn10-cross-covenant-fusion.md @@ -0,0 +1,47 @@ +# TN10 cross-covenant read/write observation / TN10 跨 Covenant 读写观察 + +## Status / 状态 + +On 2026-08-14 a community transaction was accepted on TN10 that co-spent two +different covenant lineages, continued one input, and used the other input to +authorize a fresh covenant lineage output. This is useful evidence for atomic +read/write composition, but the forum post does not publish a reproducible +source repository or compiler artifact. Studio therefore records it as an +observation and builder regression pattern, not as a deployable template or a +standard. + +2026-08-14,社区展示了一笔已被 TN10 接受的交易:共同花费两个不同 Covenant +lineage,延续其中一个输入,并由另一个 Covenant 输入授权新的 lineage 输出。这为原子 +读写组合提供了有价值的链上证据,但论坛没有给出可复现源码仓库与编译产物。因此 Studio +只把它记录为观察样本与 builder 回归模式,不把它包装成可部署模板或正式标准。 + +## Recorded evidence / 已记录证据 + +- Transaction: `975dbb2ad064ef5b4339f98e464d5f64df6af84a43995465cb3c9927b24dcf35` +- Input A covenant ID: `8614b79030c6024e61f1757a44ac803ba8c9fe314f5779c68709a672c10acdb7` +- Input B covenant ID: `2a8a728d3629da290aa08762830ef09e45692ff775dc4bb2b3b5cb435c39bdd3` +- Fresh output covenant ID reported by the transaction author: + `0fc9ff63bc367db7b8adfe463502904e0eb79b20c771156bae8f977b1d8d1f0e` +- Explorer: +- Discussion: + +## Studio builder rule / Studio builder 规则 + +`buildAtomicCovenantPackage` now accepts a covenant output with +`genesisAuthorizerCovenantId`. The builder derives the authorizing input index +from the verified input covenant ID, calls the consensus SDK's +`populateGenesisCovenants`, and records the resulting covenant ID in the +portable package. A caller-supplied input index or caller-supplied fresh +covenant ID is rejected. + +`buildAtomicCovenantPackage` 现在允许 Covenant 输出声明 +`genesisAuthorizerCovenantId`。Builder 会从已核验的输入 Covenant ID 推导授权输入位置, +调用共识 SDK 的 `populateGenesisCovenants`,并把生成的 Covenant ID 写入操作包。调用方 +不能直接指定授权 input index,也不能预先伪造新 Covenant ID。 + +This only constructs and binds the transaction. Each participating redeem +program must still independently validate the intended cross-covenant state, +script, value, cardinality and output covenant identity. + +这只完成交易构建和绑定。参与交易的每个 redeem program 仍必须独立核验预期的跨 +Covenant 状态、脚本、金额、数量以及输出 Covenant identity。 diff --git a/docs/x402-experimental-profile.md b/docs/x402-experimental-profile.md index 6fad293..902e687 100644 --- a/docs/x402-experimental-profile.md +++ b/docs/x402-experimental-profile.md @@ -1,13 +1,24 @@ # Kaspa x402 TN10 experimental profile / Kaspa x402 TN10 实验档案 -Studio tracks the public Kaspa x402 alpha at source commit -`7cae0eeea174f4ed077c96f376af69a38d18eddb`. This is an interoperability +Studio tracks Kaspa x402 `v0.1.0-alpha.10` at the peeled source commit +`78f2ada2b2dd3d54116b4cbca1fe9ec08691efc0`. This is an interoperability profile, not a mainnet-readiness claim and not an authorization source. -Studio 跟踪公开的 Kaspa x402 alpha 源码提交 -`7cae0eeea174f4ed077c96f376af69a38d18eddb`。这是互操作档案,不代表主网成熟, +Studio 跟踪 Kaspa x402 `v0.1.0-alpha.10` 对应的源码提交 +`78f2ada2b2dd3d54116b4cbca1fe9ec08691efc0`。这是互操作档案,不代表主网成熟, 也不能作为资金授权来源。 +Alpha.10 keeps `standard-native` and `additive` exact payments and replaces the +previous batch design with `kaspa-escrow-v2`. The batch profile uses a stable +KIP-20 lineage, lifetime accounting, partial claims, same-lineage top-ups, +refunds, and an explicit claim reserve. Studio records these semantics for +portable-package interoperability only; the upstream release remains TN10-only. + +Alpha.10 保留 `standard-native` 与 `additive` 单次付款,并用 +`kaspa-escrow-v2` 替换旧批量方案。批量档案使用稳定 KIP-20 lineage、全生命周期 +记账、部分领取、同 lineage 充值、退款和显式领取预留。Studio 目前只把这些语义用于 +操作包互操作档案;上游版本仍仅限 TN10。 + ## Network identifiers / 网络标识 - Studio internal: `tn10` @@ -28,9 +39,9 @@ Kaspa node network. |---|---| | payment requirements | read-only request metadata; never wallet authority | | exact payment | ordinary reviewed wallet-transfer draft | -| batch escrow deposit | compiled covenant deployment with exact artifact evidence | +| `kaspa-escrow-v2` deposit/top-up | stable KIP-20 lineage plus exact artifact evidence | | voucher | domain-separated off-chain message signature bound to network, script and active outpoint | -| claim/refund | versioned `.ssinvite` operation package and local engine preflight | +| partial claim/refund | versioned `.ssinvite` package, lifetime-accounting checks and local engine preflight | | settlement evidence | node txid plus optional Kascov visualization | Every future executable x402 template must pin the upstream source and compiler, @@ -45,4 +56,6 @@ upstream alpha escrow as a deployable built-in template. 延续、退款、重放、错误网络、错误 outpoint、价值守恒和手续费边界测试。在这些门槛完成前, Studio 不会把上游 alpha 托管合约伪装成可直接部署的成熟内置模板。 -Primary source: +Primary source: + +Release: diff --git a/index.html b/index.html index 7fb69ea..bd5ebd5 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,7 @@

契约项目

工具链 +
Studio 版本0.2.9
SilverScript Skill
本地编译器
默认网络TN10
@@ -67,6 +68,44 @@

契约项目

+
+
+
STUDIO 0.2.9 · VISIBLE CAPABILITIES

0.2.9 新功能入口

+ TN10 FIRST +
+

这里区分可直接操作的功能和仅供研究的互操作档案。点击可用功能会进入对应界面,不再把底层 API 当成已经完成的产品入口。

+
+
+ 可直接使用 +

最新编译器与兼容档案

+

使用官方 14dce9a 完整编译,并可切回 0.2.8、0.2.7 与旧版档案复现项目。

+ +
+
+ 可直接使用 +

KCC20 安全初筛

+

对当前源码检查借用接收、minter leader、Token 状态、KAS value 和共同授权边界。

+ +
+
+ TN10 实验功能 +

原子多 Covenant 构建器

+

构建 2–32 个 Covenant 输入的单笔交易,并支持由已核验输入授权创建新 lineage。

+ +
+
+ TN10 实验功能 +

KCC721 图片完整性

+

HTTPS 图片必须填写 SHA-256;IPFS 继续使用内容寻址。摘要会提交全部元数据。

+ +
+
+ 研究档案 · 不可部署 +

Kaspa x402 alpha.10

+

已记录 escrow-v2、部分领取、追加和退款语义;尚未达到内置模板的源码迁移、编译和对抗测试门槛。

+
+
+
COMPILE-VERIFIED · NO AI REQUIRED

功能模板

@@ -226,6 +265,24 @@

契约项目

+
+
ATOMIC COVENANT TRANSACTION

原子多 Covenant 构建器

TN10 · IDLE
+

粘贴完整构建描述,Studio 会核对 2–32 个不同 Covenant ID、redeem program、UTXO、金额和显式手续费。新 lineage 输出使用 genesisAuthorizerCovenantId 指向一个已核验输入;本步骤只构建并审查,不会签名或广播。

+
+
+ +
+
+
+ 构建边界 +
    +
  • 仅开放 TN10;主网请求会在界面立即拒绝。
  • +
  • 延续输出必须携带现有 covenantId,新 lineage 输出不能伪装成延续。
  • +
  • 构建成功后自动送入下方操作包审查,再由各授权钱包分别签名。
  • +
+
+
+
PORTABLE SIGNING PACKAGE

外部 Covenant 审查与签名

IDLE

可以审查并签署其他网站生成的完整交易包。只有 covenant ID/cov hash 不足以签名;包内还必须包含待签交易、完整 UTXO、redeem program、ABI、入口和参数。

diff --git a/knowledge/kaspa-silverscript/references/official-baseline.md b/knowledge/kaspa-silverscript/references/official-baseline.md index 410bbba..a3c7516 100644 --- a/knowledge/kaspa-silverscript/references/official-baseline.md +++ b/knowledge/kaspa-silverscript/references/official-baseline.md @@ -3,8 +3,8 @@ ## Verified snapshot - Repository: -- Verified commit: `6f9e078b1d8b5389212755183b592704de99fea5` -- Verified date: 2026-08-10 +- Verified commit: `14dce9a5ce8769cdfbd0c8965f8764fa9c325067` +- Verified date: 2026-08-15 - Compiler/language status: experimental - Official recommendation at this snapshot: use bytecode artifacts on testnet-10 until the first stable v1 release. @@ -15,17 +15,17 @@ deployment, compatibility, or mainnet-readiness questions. ## Primary sources - Project status and debugger: - + - Language tutorial: - + - Covenant declaration semantics: - + - Built-ins and cross-template validation: - + - KCC20 book: - Official application examples, including chess: - + ## Snapshot capabilities @@ -67,6 +67,11 @@ deployment, compatibility, or mainnet-readiness questions. integer uses checked `value as byte`. Scalar byte to integer conversion must choose `signed(byteValue)` or `unsigned(byteValue)`, and scalar bytes cannot participate directly in arithmetic operators. +- Commit `c1ed163` hardens entrypoint arguments and constant evaluation and caps + bounded-loop `max_iterations` at 10,000. +- Commit `14dce9a` rejects declaration shadowing and builtin overrides and fixes + struct assignment/lowering, ternary lowering, bool-array normalization, + `.split()` result typing, and scalar-cast validation. ## Terminology discipline diff --git a/knowledge/kaspa-silverscript/references/upstream.json b/knowledge/kaspa-silverscript/references/upstream.json index b5ae3fe..b61c754 100644 --- a/knowledge/kaspa-silverscript/references/upstream.json +++ b/knowledge/kaspa-silverscript/references/upstream.json @@ -1,8 +1,8 @@ { "repository": "https://github.com/kaspanet/silverscript.git", "branch": "master", - "verified_commit": "6f9e078b1d8b5389212755183b592704de99fea5", - "verified_at": "2026-08-10", + "verified_commit": "14dce9a5ce8769cdfbd0c8965f8764fa9c325067", + "verified_at": "2026-08-15", "status": "experimental", "recommended_network": "testnet-10" } diff --git a/package-lock.json b/package-lock.json index e4626fb..6388c5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.8", + "version": "0.2.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kaspa-silverscript-studio", - "version": "0.2.8", + "version": "0.2.9", "license": "MIT", "dependencies": { "@kluster/kaspa-wasm": "2.0.1", diff --git a/package.json b/package.json index 6b5b8fc..f47f8bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kaspa-silverscript-studio", - "version": "0.2.8", + "version": "0.2.9", "private": true, "type": "module", "description": "Local bilingual AI-assisted SilverScript contract studio for Kaspa", diff --git a/scripts/build-silverc.mjs b/scripts/build-silverc.mjs index 8d46afb..f638c98 100644 --- a/scripts/build-silverc.mjs +++ b/scripts/build-silverc.mjs @@ -8,8 +8,9 @@ import { fileURLToPath } from "node:url"; import { cargoReleaseBinary, executableName, makeExecutable } from "./platform-binaries.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const latestCommit = "6f9e078b1d8b5389212755183b592704de99fea5"; -const previousCommit = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; +const latestCommit = "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"; +const previousCommit = "6f9e078b1d8b5389212755183b592704de99fea5"; +const olderCommit = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; const legacyCommit = "2a3961cadc76bb16a425042172ffe32481da89b5"; const work = fs.mkdtempSync(path.join(os.tmpdir(), "silverstudio-silverc-")); @@ -36,6 +37,8 @@ function buildProfile({ id, commit, outputName, configuredSource }) { run("git", ["-C", repository, "checkout", "--detach", commit]); } + // Keep revision targets isolated. Cargo can otherwise reuse a same-name and + // same-version workspace artifact from a different SilverScript commit. const targetDirectory = path.join(work, `target-${id}`); run("cargo", [ "build", @@ -56,17 +59,23 @@ function buildProfile({ id, commit, outputName, configuredSource }) { try { fs.mkdirSync(path.join(root, "config"), { recursive: true }); const latestBin = buildProfile({ - id: "latest-6f9e078", + id: "latest-14dce9a", commit: latestCommit, outputName: "silverc-latest", configuredSource: process.env.SILVERSCRIPT_LATEST_SOURCE || process.env.SILVERSCRIPT_SOURCE || "" }); const previousBin = buildProfile({ - id: "latest-cb34aa5", + id: "latest-6f9e078", commit: previousCommit, - outputName: "silverc-cb34aa5", + outputName: "silverc-6f9e078", configuredSource: process.env.SILVERSCRIPT_PREVIOUS_SOURCE || "" }); + const olderBin = buildProfile({ + id: "latest-cb34aa5", + commit: olderCommit, + outputName: "silverc-cb34aa5", + configuredSource: process.env.SILVERSCRIPT_OLDER_SOURCE || "" + }); const legacyBin = buildProfile({ id: "legacy-2a3961c", commit: legacyCommit, @@ -75,22 +84,29 @@ try { }); const latestSha256 = sha256(latestBin); const previousSha256 = sha256(previousBin); + const olderSha256 = sha256(olderBin); const legacySha256 = sha256(legacyBin); const manifest = { - defaultProfileId: "latest-6f9e078", + defaultProfileId: "latest-14dce9a", profiles: { - "latest-6f9e078": { + "latest-14dce9a": { bin: latestBin, sha256: latestSha256, upstreamCommit: latestCommit, builtAt: new Date().toISOString() }, - "latest-cb34aa5": { + "latest-6f9e078": { bin: previousBin, sha256: previousSha256, upstreamCommit: previousCommit, builtAt: new Date().toISOString() }, + "latest-cb34aa5": { + bin: olderBin, + sha256: olderSha256, + upstreamCommit: olderCommit, + builtAt: new Date().toISOString() + }, "legacy-2a3961c": { bin: legacyBin, sha256: legacySha256, @@ -102,11 +118,14 @@ try { fs.writeFileSync(path.join(root, "config", "compiler.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 }); run(latestBin, ["--help"]); run(previousBin, ["--help"]); + run(olderBin, ["--help"]); run(legacyBin, ["--help"]); console.log(`latest silverc commit: ${latestCommit}`); console.log(`latest silverc sha256: ${latestSha256}`); console.log(`previous silverc commit: ${previousCommit}`); console.log(`previous silverc sha256: ${previousSha256}`); + console.log(`older silverc commit: ${olderCommit}`); + console.log(`older silverc sha256: ${olderSha256}`); console.log(`legacy silverc commit: ${legacyCommit}`); console.log(`legacy silverc sha256: ${legacySha256}`); console.log(`manifest: ${path.join(root, "config", "compiler.json")}`); diff --git a/scripts/prepare-desktop-runtime.mjs b/scripts/prepare-desktop-runtime.mjs index 2c185df..090809f 100644 --- a/scripts/prepare-desktop-runtime.mjs +++ b/scripts/prepare-desktop-runtime.mjs @@ -16,7 +16,7 @@ function copy(relative) { fs.cpSync(path.join(root, relative), destination, { recursive: true, force: true }); } -const helperNames = ["silverc-latest", "silverc-cb34aa5", "silverc-legacy", "kascov-preflight"]; +const helperNames = ["silverc-latest", "silverc-6f9e078", "silverc-cb34aa5", "silverc-legacy", "kascov-preflight"]; for (const helper of helperNames) { const source = path.join(root, binaryRelativePath(helper)); if (!fs.existsSync(source)) throw new Error(`Pinned ${executableName(helper)} is missing. Run the matching setup command first.`); diff --git a/server/atomic-covenant-builder.mjs b/server/atomic-covenant-builder.mjs index 8ac0a36..93708ee 100644 --- a/server/atomic-covenant-builder.mjs +++ b/server/atomic-covenant-builder.mjs @@ -53,19 +53,34 @@ function outputFrom(descriptor, network, inputIndexById) { const value = BigInt(descriptor.valueSompi); if (value <= 0n) throw builderError("Every atomic output must have positive value"); if (descriptor.address) { + if (descriptor.genesisAuthorizerCovenantId) throw builderError("An address output cannot also be a fresh covenant lineage"); const address = String(descriptor.address).trim().toLowerCase(); if (!address.startsWith(`${network.prefix}:`)) throw builderError("Atomic output address is on the wrong network"); - return new kaspa.TransactionOutput(value, kaspa.payToAddressScript(address)); + return { output: new kaspa.TransactionOutput(value, kaspa.payToAddressScript(address)), genesisAuthorizingInput: null }; } const programHex = cleanHex(descriptor.programHex); + if (descriptor.genesisAuthorizerCovenantId) { + if (descriptor.covenantId) throw builderError("A fresh covenant lineage output must not declare a pre-existing covenant ID"); + const authorizerId = cleanHex(descriptor.genesisAuthorizerCovenantId, 32); + const authorizingInput = inputIndexById.get(authorizerId); + if (!Number.isSafeInteger(authorizingInput)) throw builderError("Fresh covenant lineage authorizer does not map to an input covenant ID"); + return { + output: new kaspa.TransactionOutput(value, kaspa.payToScriptHashScript(programHex)), + genesisAuthorizingInput: authorizingInput, + programSha256: sha256(Buffer.from(programHex, "hex")) + }; + } const covenantId = cleanHex(descriptor.covenantId, 32); const sourceInputIndex = inputIndexById.get(covenantId); if (!Number.isSafeInteger(sourceInputIndex)) throw builderError("Covenant continuation output does not map to an input covenant ID"); - return new kaspa.TransactionOutput( - value, - kaspa.payToScriptHashScript(programHex), - new kaspa.CovenantBinding(sourceInputIndex, new kaspa.Hash(covenantId)) - ); + return { + output: new kaspa.TransactionOutput( + value, + kaspa.payToScriptHashScript(programHex), + new kaspa.CovenantBinding(sourceInputIndex, new kaspa.Hash(covenantId)) + ), + genesisAuthorizingInput: null + }; } export function buildAtomicCovenantPackage({ network: networkId = "tn10", covenantInputs, outputs, p2pkAuthorization = null, feeSompi, provenance = {} }) { @@ -147,7 +162,8 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena transactionInputs.push(authInput); } const inputIndexById = new Map(metadata.map((item) => [item.covenantId, item.index])); - const transactionOutputs = outputs.map((output) => outputFrom(output, network, inputIndexById)); + const outputPlans = outputs.map((output) => outputFrom(output, network, inputIndexById)); + const transactionOutputs = outputPlans.map((plan) => plan.output); const inputTotal = covenantInputs.reduce((sum, item) => sum + amountOf(item.utxo), 0n) + (p2pkAuthorization?.input ? amountOf(p2pkAuthorization.input.utxo) : 0n); const outputTotal = outputs.reduce((sum, output) => sum + BigInt(output.valueSompi), 0n); @@ -163,6 +179,28 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena gas: 0n, payload: "" }); + const genesisGroups = new Map(); + outputPlans.forEach((plan, outputIndex) => { + if (!Number.isSafeInteger(plan.genesisAuthorizingInput)) return; + const indexes = genesisGroups.get(plan.genesisAuthorizingInput) || []; + indexes.push(outputIndex); + genesisGroups.set(plan.genesisAuthorizingInput, indexes); + }); + if (genesisGroups.size) { + transaction.populateGenesisCovenants([...genesisGroups].map(([authorizingInput, genesisOutputs]) => ({ + authorizingInput, + outputs: genesisOutputs + }))); + } + const genesisCovenants = outputPlans.flatMap((plan, outputIndex) => { + if (!Number.isSafeInteger(plan.genesisAuthorizingInput)) return []; + const covenantId = String(transaction.outputs[outputIndex].covenant?.covenantId || "").toLowerCase(); + if (!/^[0-9a-f]{64}$/.test(covenantId)) throw builderError("Fresh covenant lineage ID was not populated", "GENESIS_COVENANT_POPULATION_FAILED"); + if (Number(transaction.outputs[outputIndex].covenant?.authorizingInput) !== plan.genesisAuthorizingInput) { + throw builderError("Fresh covenant lineage has the wrong authorizing input", "GENESIS_COVENANT_AUTHORIZATION_MISMATCH"); + } + return [{ outputIndex, authorizingInputIndex: plan.genesisAuthorizingInput, covenantId, programSha256: plan.programSha256 }]; + }); const covenantSignatures = metadata.reduce((sum, item) => sum + (item.arguments || []).filter((argument) => argument?.kind === "signature").length, 0); const sigOps = Math.max(1, covenantSignatures + (p2pkAuthorization?.input ? 1 : 0)); if (!kaspa.updateTransactionMass(network.kaspaNetworkId, transaction, sigOps, true)) throw builderError("Atomic covenant transaction exceeds the current mass limit", "ATOMIC_MASS_LIMIT"); @@ -172,11 +210,13 @@ export function buildAtomicCovenantPackage({ network: networkId = "tn10", covena networkCaip2: caip2Network(network.id), transactionSafeJson: transaction.serializeToSafeJSON(), covenantInputs: metadata, + ...(genesisCovenants.length ? { genesisCovenants } : {}), ...(p2pkAuthorization?.metadata ? { p2pkAuthorization: { ...p2pkAuthorization.metadata, signed: false } } : {}), provenance: { kind: "silverstudio-atomic-covenant", atomic: true, inputCount: metadata.length, + genesisOutputCount: genesisCovenants.length, ...provenance } }; diff --git a/server/compiler.mjs b/server/compiler.mjs index 34fcb97..853f0ef 100644 --- a/server/compiler.mjs +++ b/server/compiler.mjs @@ -6,6 +6,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { config } from "./config.mjs"; import { boundedText, sha256 } from "./security.mjs"; +import { analyzeKcc20Source } from "./kcc20-security.mjs"; const execFileAsync = promisify(execFile); @@ -65,7 +66,7 @@ function lineAt(source, offset) { } function latestHardeningFindings(source, target) { - if (!["cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "6f9e078b1d8b5389212755183b592704de99fea5"].includes(target.upstreamCommit)) return []; + if (!["cb34aa5e6a598f9e461c4ad7014279ba89251d8d", "6f9e078b1d8b5389212755183b592704de99fea5", "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"].includes(target.upstreamCommit)) return []; const findings = []; const declarations = new Map(); const functionPattern = /\b(?:entry|function)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g; @@ -136,7 +137,7 @@ function latestHardeningFindings(source, target) { } function scalarConversionFindings(source, target) { - if (target.upstreamCommit !== "6f9e078b1d8b5389212755183b592704de99fea5") return []; + if (!["6f9e078b1d8b5389212755183b592704de99fea5", "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"].includes(target.upstreamCommit)) return []; const findings = []; const scalarByteCast = /\bbyte\s*\(\s*([^()\n]+?)\s*\)/g; let match; @@ -349,6 +350,7 @@ export async function staticAnalyze(source) { message: "review fee-output aliasing across multiple contract executions" }); } + findings.push(...analyzeKcc20Source(text)); return { kind: "heuristic-triage", findings, diff --git a/server/config.mjs b/server/config.mjs index 2233f12..f73c705 100644 --- a/server/config.mjs +++ b/server/config.mjs @@ -8,8 +8,9 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); dotenv.config({ path: path.join(ROOT, ".env") }); dotenv.config({ path: path.join(ROOT, ".env.local"), override: true }); -export const SILVERSCRIPT_COMMIT = "6f9e078b1d8b5389212755183b592704de99fea5"; -export const SILVERSCRIPT_PREVIOUS_COMMIT = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; +export const SILVERSCRIPT_COMMIT = "14dce9a5ce8769cdfbd0c8965f8764fa9c325067"; +export const SILVERSCRIPT_PREVIOUS_COMMIT = "6f9e078b1d8b5389212755183b592704de99fea5"; +export const SILVERSCRIPT_OLDER_COMMIT = "cb34aa5e6a598f9e461c4ad7014279ba89251d8d"; export const SILVERSCRIPT_LEGACY_COMMIT = "2a3961cadc76bb16a425042172ffe32481da89b5"; export const NETWORKS = Object.freeze({ @@ -56,12 +57,17 @@ function loadCompilerConfig() { ? stored : {}; const isPrevious = definition.upstreamCommit === SILVERSCRIPT_PREVIOUS_COMMIT; + const isOlder = definition.upstreamCommit === SILVERSCRIPT_OLDER_COMMIT; const environmentBin = isLatest ? process.env.SILVERC_LATEST_BIN || process.env.SILVERC_BIN - : isPrevious ? process.env.SILVERC_PREVIOUS_BIN : process.env.SILVERC_LEGACY_BIN; + : isPrevious ? process.env.SILVERC_PREVIOUS_BIN || process.env.SILVERC_6F9E078_BIN + : isOlder ? process.env.SILVERC_OLDER_BIN || process.env.SILVERC_CB34AA5_BIN + : process.env.SILVERC_LEGACY_BIN; const environmentSha = isLatest ? process.env.SILVERC_LATEST_SHA256 || process.env.SILVERC_SHA256 - : isPrevious ? process.env.SILVERC_PREVIOUS_SHA256 : process.env.SILVERC_LEGACY_SHA256; + : isPrevious ? process.env.SILVERC_PREVIOUS_SHA256 || process.env.SILVERC_6F9E078_SHA256 + : isOlder ? process.env.SILVERC_OLDER_SHA256 || process.env.SILVERC_CB34AA5_SHA256 + : process.env.SILVERC_LEGACY_SHA256; return [definition.id, Object.freeze({ ...definition, bin: path.resolve(environmentBin || local.bin || legacyStored.bin || path.join(ROOT, definition.binary)), @@ -71,7 +77,7 @@ function loadCompilerConfig() { })); const defaultProfileId = profiles[stored.defaultProfileId] ? stored.defaultProfileId - : compatibility.defaultProfileId || "latest-6f9e078"; + : compatibility.defaultProfileId || "latest-14dce9a"; return Object.freeze({ defaultProfileId, profiles: Object.freeze(profiles), diff --git a/server/kcc20-security.mjs b/server/kcc20-security.mjs new file mode 100644 index 0000000..87fbc21 --- /dev/null +++ b/server/kcc20-security.mjs @@ -0,0 +1,78 @@ +function finding(code, severity, line, messageZh, messageEn) { + return { code, severity, line, messageZh, messageEn, message: messageEn }; +} + +function lineAt(source, offset) { + return source.slice(0, Math.max(0, offset)).split("\n").length; +} + +function hasAll(source, patterns) { + return patterns.every((pattern) => pattern.test(source)); +} + +export function analyzeKcc20Source(source) { + const text = String(source || ""); + const stateShape = [/\bownerIdentifier\b/, /\bidentifierType\b/, /\bamount\b/, /\bisMinter\b/]; + if (!hasAll(text, stateShape)) return []; + + const findings = []; + const borrowed = /\bBORROWED_RECEIVE\b|\bborrow(?:ed|ing)?\b/i.exec(text); + if (borrowed) { + const rejectsMinterLeader = /require\s*\(\s*!\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.isMinter\s*\)/.test(text); + if (!rejectsMinterLeader) { + findings.push(finding( + "KCC201", + "error", + lineAt(text, borrowed.index), + "检测到免签借用接收路径,但没有明确拒绝 minter leader;该组合可能让任意调用者保留铸币权并增发。", + "A borrowed-receive path is present without an explicit minter-leader rejection; an unauthenticated leader may retain mint authority and inflate supply." + )); + } + + const preservesOwner = /(?:newStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.ownerIdentifier\s*==\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.ownerIdentifier/.test(text); + const preservesType = /(?:newStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.identifierType\s*==\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.identifierType/.test(text); + const amountIncreases = /(?:newStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.amount\s*>\s*(?:prevStates\s*\[[^\]]+\]|[A-Za-z_]\w*)\.amount/.test(text); + if (!preservesOwner || !preservesType || !amountIncreases) { + findings.push(finding( + "KCC202", + "error", + lineAt(text, borrowed.index), + "借用接收必须保持 ownerIdentifier 和 identifierType 不变,并要求 token amount 严格增加。", + "Borrowed receive must preserve ownerIdentifier and identifierType and require the token amount to increase strictly." + )); + } + + const preservesKas = /OpCovOutputIdx\s*\([\s\S]{0,240}?\.value\s*>=\s*[\s\S]{0,160}?OpCovInputIdx/.test(text) + || /tx\.outputs\s*\[[^\]]+\]\.value\s*>=\s*tx\.inputs\s*\[[^\]]+\]\.value/.test(text); + if (!preservesKas) { + findings.push(finding( + "KCC203", + "error", + lineAt(text, borrowed.index), + "借用接收没有明显保持或增加被借用 UTXO 的 KAS value;这可能把接收者的存储押金转走。", + "Borrowed receive does not visibly preserve or increase the borrowed UTXO KAS value, which may drain the recipient's storage deposit." + )); + } + + findings.push(finding( + "KCC204", + "manual-review", + lineAt(text, borrowed.index), + "借用会生成新的 outpoint,令旧的预签交易失效。若需要可借用后继续授权,应使用带域分离、网络、Covenant ID、lineage、nonce、收款方和金额的 checkMsgSig 意图,并做重放测试。", + "Borrowing creates a new outpoint and invalidates old pre-signed transactions. Borrow-surviving authorization needs a replay-tested checkMsgSig intent bound to a domain, network, covenant ID, lineage, nonce, recipient and amount." + )); + } + + const presenceAuthorization = /IDENTIFIER_(?:SCRIPT_HASH|COVENANT_ID)[\s\S]{0,900}?(?:OpInputCovenantId|scriptPublicKey)/.exec(text); + if (presenceAuthorization) { + findings.push(finding( + "KCC205", + "manual-review", + lineAt(text, presenceAuthorization.index), + "共同输入的存在只能证明控制主体参与了交易,不能自动证明它专门同意这次 token 转移;请审查共同花费脚本是否承诺完整交易或明确意图。", + "A co-present control input proves participation, not specific consent to this token transfer; review whether its spend commits to the complete transaction or an explicit intent." + )); + } + + return findings; +} diff --git a/server/project-store.mjs b/server/project-store.mjs index 35a7089..599107c 100644 --- a/server/project-store.mjs +++ b/server/project-store.mjs @@ -52,7 +52,7 @@ export class ProjectStore { requirements: String(input.requirements || ""), source: String(input.source || SAMPLE_SOURCE), constructorArgs: Array.isArray(input.constructorArgs) ? input.constructorArgs : [], - compilerProfileId: String(input.compilerProfileId || "latest-6f9e078"), + compilerProfileId: String(input.compilerProfileId || "latest-14dce9a"), templateParameters: input.templateParameters && typeof input.templateParameters === "object" ? input.templateParameters : {}, deployAmount: String(input.deployAmount || "0.5"), specification: input.specification || null, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 40231b7..144d5f6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -66,7 +66,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "app" -version = "0.2.8" +version = "0.2.9" dependencies = [ "log", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a688aee..11ecb65 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.2.8" +version = "0.2.9" description = "Local-first Kaspa SilverScript covenant workbench" authors = ["w00c00"] license = "MIT" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cc00ee9..19054f3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,7 +35,7 @@ fn append_backend_log(path: &Path, message: &str) { } } -fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf), String> { +fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf, PathBuf), String> { let resource_dir = app .path() .resource_dir() @@ -44,6 +44,7 @@ fn runtime_paths(app: &AppHandle) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, Ok(( resource_dir.join("runtime/app/server/index.mjs"), resource_dir.join(format!("runtime/app/bin/silverc-latest{executable_suffix}")), + resource_dir.join(format!("runtime/app/bin/silverc-6f9e078{executable_suffix}")), resource_dir.join(format!( "runtime/app/bin/silverc-cb34aa5{executable_suffix}" )), @@ -78,12 +79,13 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { let log_path = app_data_dir.join("backend.log"); *app.state::().log_path.lock().unwrap() = Some(log_path.clone()); - let (script, latest_compiler, previous_compiler, legacy_compiler, preflight_engine) = + let (script, latest_compiler, previous_compiler, older_compiler, legacy_compiler, preflight_engine) = runtime_paths(app)?; let required = [ &script, &latest_compiler, &previous_compiler, + &older_compiler, &legacy_compiler, &preflight_engine, ]; @@ -114,6 +116,7 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { let data_arg = child_process_path(&app_data_dir); let latest_compiler_arg = child_process_path(&latest_compiler); let previous_compiler_arg = child_process_path(&previous_compiler); + let older_compiler_arg = child_process_path(&older_compiler); let legacy_compiler_arg = child_process_path(&legacy_compiler); let preflight_engine_arg = child_process_path(&preflight_engine); @@ -127,6 +130,7 @@ fn spawn_backend(app: &AppHandle) -> Result<(), String> { .env("STUDIO_DATA_DIR", data_arg) .env("SILVERC_LATEST_BIN", latest_compiler_arg) .env("SILVERC_PREVIOUS_BIN", previous_compiler_arg) + .env("SILVERC_OLDER_BIN", older_compiler_arg) .env("SILVERC_LEGACY_BIN", legacy_compiler_arg) .env("KASCOV_PREFLIGHT_BIN", preflight_engine_arg) .spawn() diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 40dfbcb..5c0ec5a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Kaspa SilverScript Studio", - "version": "0.2.8", + "version": "0.2.9", "identifier": "io.kaspa.silverscript-studio", "build": { "frontendDist": "../dist", diff --git a/src/kcc721-metadata.js b/src/kcc721-metadata.js index b8ce751..f72951d 100644 --- a/src/kcc721-metadata.js +++ b/src/kcc721-metadata.js @@ -11,6 +11,12 @@ function cleanText(value, label, maximum, required = false) { return text; } +function cleanImageHash(value) { + const text = String(value ?? "").trim().toLowerCase().replace(/^0x/, ""); + if (text && !/^[0-9a-f]{64}$/.test(text)) metadataError("NFT image SHA-256 must contain exactly 64 hexadecimal characters"); + return text; +} + function stable(value) { if (Array.isArray(value)) return value.map(stable); if (!value || typeof value !== "object") return value; @@ -26,6 +32,9 @@ export function canonicalKcc721Metadata(value) { const name = cleanText(input.name, "NFT name", 120, true); const description = cleanText(input.description, "NFT description", 2_000); const image = cleanText(input.image, "NFT image URI", 2_048); + const imageHash = cleanImageHash(input.imageHash ?? input.image_hash); + if (image && !/^(?:https:\/\/|ipfs:\/\/)/i.test(image)) metadataError("NFT image URI must use https:// or ipfs://"); + if (/^https:\/\//i.test(image) && !imageHash) metadataError("HTTPS NFT images require an immutable image SHA-256"); const externalUrl = cleanText(input.externalUrl ?? input.external_url, "NFT external URL", 2_048); let attributes = input.attributes ?? []; if (typeof attributes === "string") { @@ -45,6 +54,7 @@ export function canonicalKcc721Metadata(value) { name, ...(description ? { description } : {}), ...(image ? { image } : {}), + ...(imageHash ? { image_hash: imageHash } : {}), ...(externalUrl ? { external_url: externalUrl } : {}), ...(attributes.length ? { attributes } : {}) }; diff --git a/src/main.js b/src/main.js index bcc10f5..33bf50d 100644 --- a/src/main.js +++ b/src/main.js @@ -17,7 +17,13 @@ const copy = { connectWallet: "连接钱包", localWorkspace: "本地工作区", projects: "契约项目", toolchain: "工具链", skillKnowledge: "SilverScript Skill", compiler: "本地编译器", defaultNetwork: "默认网络", localSafety: "源码和项目保存在本机。AI 密钥仅通过本机回环写入加密保险库,钱包私钥永不离开钱包。", - savedLocal: "已保存到本机", design: "需求设计", source: "契约源码", compile: "编译审查", deploy: "签名上链", operationCenter: "操作中心", operationCenterHelp: "统一处理续签、多签、领取、退款和确认付款。导入操作包后会自动识别对应界面,但签名前仍必须核对程序哈希、交易承诺和全部输出。", + savedLocal: "已保存到本机", design: "需求设计", source: "契约源码", compile: "编译审查", deploy: "签名上链", operationCenter: "操作中心", operationCenterHelp: "统一处理续签、多签、领取、退款和确认付款。导入操作包后会自动识别对应界面,但签名前仍必须核对程序哈希、交易承诺和全部输出。", studioVersion: "Studio 版本", + releaseHubTitle: "0.2.9 新功能入口", releaseHubHelp: "这里区分可直接操作的功能和仅供研究的互操作档案。点击可用功能会进入对应界面,不再把底层 API 当成已经完成的产品入口。", availableNow: "可直接使用", tn10Experimental: "TN10 实验功能", researchProfile: "研究档案 · 不可部署", + latestCompilerTitle: "最新编译器与兼容档案", latestCompilerHelp: "使用官方 14dce9a 完整编译,并可切回 0.2.8、0.2.7 与旧版档案复现项目。", openCompiler: "打开编译审查", + kcc20GuardTitle: "KCC20 安全初筛", kcc20GuardHelp: "对当前源码检查借用接收、minter leader、Token 状态、KAS value 和共同授权边界。", scanCurrentSource: "检查当前源码", + atomicBuilderTitle: "原子多 Covenant 构建器", atomicBuilderShortHelp: "构建 2–32 个 Covenant 输入的单笔交易,并支持由已核验输入授权创建新 lineage。", openAtomicBuilder: "打开构建器", + kcc721IntegrityTitle: "KCC721 图片完整性", kcc721IntegrityHelp: "HTTPS 图片必须填写 SHA-256;IPFS 继续使用内容寻址。摘要会提交全部元数据。", openKcc721: "打开 KCC721 模板", x402ProfileHelp: "已记录 escrow-v2、部分领取、追加和退款语义;尚未达到内置模板的源码迁移、编译和对抗测试门槛。", + atomicBuilderHelp: "粘贴完整构建描述,Studio 会核对 2–32 个不同 Covenant ID、redeem program、UTXO、金额和显式手续费。新 lineage 输出使用 genesisAuthorizerCovenantId 指向一个已核验输入;本步骤只构建并审查,不会签名或广播。", atomicBuildJson: "构建描述 JSON", fillFormatExample: "填入格式示例", buildAndReviewAtomic: "构建并进入审查", atomicBoundaryTitle: "构建边界", atomicBoundaryOne: "仅开放 TN10;主网请求会在界面立即拒绝。", atomicBoundaryTwo: "延续输出必须携带现有 covenantId,新 lineage 输出不能伪装成延续。", atomicBoundaryThree: "构建成功后自动送入下方操作包审查,再由各授权钱包分别签名。", describeContract: "描述你需要的契约", requirementsPlaceholder: "例如:双方各锁定 100 TKAS,胜负由双方签名确认;超时后任何一方可以退款……", aiProvider: "AI 接口", model: "模型", generate: "生成规范与契约", reviewCurrent: "审查当前源码", aiBoundary: "AI 只生成候选方案。必须经过本地编译、静态检查、交易预检和人工确认后才能签名。", @@ -56,7 +62,13 @@ const copy = { connectWallet: "Connect wallet", localWorkspace: "Local workspace", projects: "Contract projects", toolchain: "Toolchain", skillKnowledge: "SilverScript Skill", compiler: "Local compiler", defaultNetwork: "Default network", localSafety: "Source and projects stay local. AI keys only cross the local loopback into the encrypted vault, and wallet keys never leave the wallet.", - savedLocal: "Saved locally", design: "Design", source: "Contract source", compile: "Compile & review", deploy: "Sign & deploy", operationCenter: "Operation Center", operationCenterHelp: "Handle renewals, multisig, claims, refunds and payment confirmations in one place. Imported packages select the matching interface automatically, but you must still verify the program hash, commitment and every output.", + savedLocal: "Saved locally", design: "Design", source: "Contract source", compile: "Compile & review", deploy: "Sign & deploy", operationCenter: "Operation Center", operationCenterHelp: "Handle renewals, multisig, claims, refunds and payment confirmations in one place. Imported packages select the matching interface automatically, but you must still verify the program hash, commitment and every output.", studioVersion: "Studio version", + releaseHubTitle: "What's new in 0.2.9", releaseHubHelp: "This area separates features you can operate now from research-only interoperability profiles. Available actions open their real workflow instead of presenting a backend API as a finished product surface.", availableNow: "Available now", tn10Experimental: "TN10 experimental", researchProfile: "Research profile · not deployable", + latestCompilerTitle: "Latest compiler and profiles", latestCompilerHelp: "Fully compile with official 14dce9a, or select the isolated 0.2.8, 0.2.7, and legacy profiles for reproducibility.", openCompiler: "Open compile review", + kcc20GuardTitle: "KCC20 security triage", kcc20GuardHelp: "Scan the current source for borrowed receive, minter leaders, token state, KAS value, and co-spend authorization boundaries.", scanCurrentSource: "Scan current source", + atomicBuilderTitle: "Atomic multi-Covenant builder", atomicBuilderShortHelp: "Build one transaction from 2–32 Covenant inputs, including fresh lineages authorized by verified inputs.", openAtomicBuilder: "Open builder", + kcc721IntegrityTitle: "KCC721 image integrity", kcc721IntegrityHelp: "HTTPS images require SHA-256; IPFS remains content-addressed. The digest commits to all metadata.", openKcc721: "Open KCC721 template", x402ProfileHelp: "The escrow-v2, partial claim, top-up, and refund semantics are recorded, but source migration, compilation, and adversarial-test admission gates are not yet satisfied.", + atomicBuilderHelp: "Paste a complete build descriptor. Studio verifies 2–32 distinct Covenant IDs, redeem programs, UTXOs, amounts, and the explicit fee. A fresh lineage uses genesisAuthorizerCovenantId to reference one verified input. This step only builds and reviews; it never signs or broadcasts.", atomicBuildJson: "Build descriptor JSON", fillFormatExample: "Fill format example", buildAndReviewAtomic: "Build and review", atomicBoundaryTitle: "Builder boundaries", atomicBoundaryOne: "TN10 only; mainnet requests are rejected immediately by the UI.", atomicBoundaryTwo: "Continuation outputs require an existing covenantId; fresh lineages cannot masquerade as continuations.", atomicBoundaryThree: "Successful builds enter the package review below, where each authorized wallet signs separately.", describeContract: "Describe the contract you need", requirementsPlaceholder: "Example: both parties lock 100 TKAS; a mutually signed result releases the pot, with a timeout refund path…", aiProvider: "AI provider", model: "Model", generate: "Generate spec & contract", reviewCurrent: "Review current source", aiBoundary: "AI produces a candidate only. Local compilation, static checks, transaction preflight and human confirmation are required before signing.", @@ -255,7 +267,7 @@ function projectPayload() { requirements: $("#requirements").value, source: $("#source-editor").value, constructorArgs, - compilerProfileId: $("#compiler-profile").value || state.config?.compiler?.defaultProfileId || "latest-6f9e078", + compilerProfileId: $("#compiler-profile").value || state.config?.compiler?.defaultProfileId || "latest-14dce9a", templateParameters: state.project?.templateParameters || {}, deployAmount: $("#deploy-amount").value, specification: state.project?.specification || null, @@ -323,7 +335,7 @@ function loadProjectIntoUi(project) { $("#requirements").value = project.requirements || ""; $("#source-editor").value = project.source || ""; $("#constructor-args").value = JSON.stringify(project.constructorArgs || [], null, 2); - $("#compiler-profile").value = project.compilerProfileId || project.artifact?.compiler?.id || project.review?.compilerProfileId || state.config?.compiler?.defaultProfileId || "latest-6f9e078"; + $("#compiler-profile").value = project.compilerProfileId || project.artifact?.compiler?.id || project.review?.compilerProfileId || state.config?.compiler?.defaultProfileId || "latest-14dce9a"; renderCompilerProfileHelp(); $("#deploy-amount").value = Number(project.deployAmount || 0) >= 0.5 ? project.deployAmount : "0.5"; $("#deploy-network").value = project.network || "tn10"; @@ -437,7 +449,7 @@ function initializeTemplateValues(template) { if (values[field.id] !== undefined && values[field.id] !== "") continue; if (field.type === "heirs") values[field.id] = [{ address: "", shareBps: 5000 }, { address: "", shareBps: 5000 }]; else if (field.type === "duration") values[field.id] = { value: String(field.defaultValue || 1), unit: field.defaultUnit || "days" }; - else if (field.type === "kcc721Metadata") values[field.id] = structuredClone(field.default || { name: "", description: "", image: "", externalUrl: "", attributes: [] }); + else if (field.type === "kcc721Metadata") values[field.id] = structuredClone(field.default || { name: "", description: "", image: "", imageHash: "", externalUrl: "", attributes: [] }); else if (field.default !== undefined) values[field.id] = String(field.default); else if (field.defaultOffsetSeconds) values[field.id] = localDateTimeValue(Date.now() + Number(field.defaultOffsetSeconds) * 1000); else if (field.type === "address" && field.useConnectedWallet && state.wallet?.network === $("#deploy-network").value) values[field.id] = state.wallet.address; @@ -490,6 +502,7 @@ function renderTemplateParameterFields(template) {